Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\AmazonVendorDF;
use Xentral\Components\SchemaCreator\Collection\SchemaCollection;
use Xentral\Components\SchemaCreator\Index;
use Xentral\Components\SchemaCreator\Option\TableOption;
use Xentral\Components\SchemaCreator\Schema\TableSchema;
use Xentral\Components\SchemaCreator\Type;
use Xentral\Core\DependencyInjection\ContainerInterface;
final class Bootstrap
{
/**
* @return array
*/
public static function registerServices(): array
{
return [
'PurchaseOrderInformationRepository' => 'onInitPurchaseOrderInformationRepository',
];
}
/**
* @param ContainerInterface $container
*
* @return PurchaseOrderInformationRepository
*/
public static function onInitPurchaseOrderInformationRepository(ContainerInterface $container
): PurchaseOrderInformationRepository {
return new PurchaseOrderInformationRepository(
$container->get('Database')
);
}
}
@@ -0,0 +1,89 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
use Xentral\Modules\AmazonVendorDF\Exception\MissingInformationException;
class AcknowledgementItem
{
/**
* Shipping 100 percent of ordered product
*
* @var string
*/
const CODE_ACCEPTED = '00';
/**
* Canceled out of stock
*
* @var string
*/
const CODE_REJECT_OUT_OF_STOCK = '03';
/**
* No article found for SKU
*
* @var string
*/
const CODE_REJECT_INVALID_SKU = '02';
const AVAILABLE_CODES = [
'00' => 'Shipping 100 percent of ordered product',
'02' => 'Canceled due to missing/invalid SKU',
'03' => 'Canceled out of stock',
'04' => 'Canceled due to duplicate Amazon Ship ID',
'05' => 'Canceled due to missing/invalid Bill To Location Code',
'06' => 'Canceled due to missing/invalid Ship From Location Code',
'07' => 'Canceled due to missing/invalid Customer Ship to Name',
'08' => 'Canceled due to missing/invalid Customer Ship to Address Line 1',
'10' => 'Canceled due to missing/invalid Customer Ship to City',
'11' => 'Canceled due to missing/invalid Customer Ship to State',
'12' => 'Canceled due to missing/invalid Customer Ship to Postal Code',
'13' => 'Canceled due to missing/invalid Customer Ship to Country Code',
'20' => 'Canceled due to missing/invalid Shipping Carrier/Shipping Method',
'21' => 'Canceled due to missing/invalid Ship to Address Line 2',
'22' => 'Canceled due to missing/invalid Ship to Address Line 3',
'50' => 'Canceled due to Tax Nexus Issue',
'51' => 'Canceled due to Restricted SKU/Qty',
];
/** @var PurchaseOrderItem */
private $item;
/** @var string */
private $code;
public function __construct(PurchaseOrderItem $item, string $code)
{
$this->item = $item;
$this->code = $code;
}
public function isRejected(): bool
{
return $this->code !== self::CODE_ACCEPTED;
}
public function isAccepted(): bool
{
return $this->code === self::CODE_ACCEPTED;
}
public function getStatusCode(): string
{
return $this->code;
}
public function toArray(): array
{
$data = [
'itemSequenceNumber' => $this->item->getItemSequenceNumber(),
'buyerProductIdentifier' => $this->item->getBuyerProductIdentifier(),
'vendorProductIdentifier' => $this->item->getVendorProductIdentifier(),
'acknowledgedQuantity' => $this->item->getQuantity()->toArray(),
];
unset($data['acknowledgedQuantity']['unitSize']);
return $data;
}
}
@@ -0,0 +1,121 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class Address
{
/** @var string */
private $name;
/** @var array */
private $addressLines;
/** @var string */
private $city;
/** @var string */
private $countryCode;
/** @var string */
private $postalCode;
/** @var string */
private $stateOrRegion;
/** @var string */
private $phone;
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function setAddressLines(array $addressLines): self
{
$this->addressLines = $addressLines;
return $this;
}
public function setCity(string $city): self
{
$this->city = $city;
return $this;
}
public function setCountryCode(string $countryCode): self
{
$this->countryCode = $countryCode;
return $this;
}
public function setPostalCode(string $postalCode): self
{
$this->postalCode = $postalCode;
return $this;
}
public function setStateOrRegion(string $stateOrRegion): self
{
$this->stateOrRegion = $stateOrRegion;
return $this;
}
public function setPhone(string $phone): self
{
$this->phone = $phone;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function getAddressLines(): array
{
return $this->addressLines;
}
public function getCity(): string
{
return $this->city;
}
public function getCountryCode(): string
{
return $this->countryCode;
}
public function getPostalCode(): string
{
return $this->postalCode;
}
public function getStateOrRegion(): string
{
return $this->stateOrRegion;
}
public function getPhone(): string
{
return $this->phone;
}
public function toArray(): array
{
return array_filter([
'name' => $this->name,
'addressLine1' => $this->addressLines[0],
'addressLine2' => $this->addressLines[1],
'addressLine3' => $this->addressLines[2],
'city' => $this->city,
'stateOrRegion' => $this->stateOrRegion,
'postalCode' => $this->postalCode,
'countryCode' => $this->countryCode,
'phone' => $this->phone,
]);
}
}
@@ -0,0 +1,75 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class Container
{
/** @var string */
private $containerType;
/** @var string */
private $containerIdentifier;
/** @var string */
private $length;
/** @var string */
private $width;
/** @var string */
private $height;
/** @var string */
private $unitOfMeasure;
/** @var array */
private $items = [];
public function __construct(string $containerIdentifier, string $containerType = 'carton')
{
$this->containerIdentifier = $containerIdentifier;
$this->containerType = $containerType;
}
public function setDimensions(string $length, string $width, string $height, string $unitOfMeasure = 'CM')
{
$this->length = $length;
$this->width = $width;
$this->height = $height;
$this->unitOfMeasure = $unitOfMeasure;
}
public function addItem(
string $itemSequenceNumber,
string $buyerProductIdentifier,
string $vendorProductIdentifier,
Quantity $packedQuantity
) {
$quantity = $packedQuantity->toArray();
unset($quantity['unitSize']);
$this->items[] = [
'itemSequenceNumber' => (int)$itemSequenceNumber,
'buyerProductIdentifier' => $buyerProductIdentifier,
'vendorProductIdentifier' => $vendorProductIdentifier,
'packedQuantity' => $quantity,
];
}
public function getItems(): array
{
return $this->items;
}
public function toArray()
{
return [
'containerType' => $this->containerType,
'containerIdentifier' => $this->containerIdentifier,
'dimensions' => [
'length' => $this->length,
'width' => $this->width,
'height' => $this->height,
'unitOfMeasure' => $this->unitOfMeasure,
],
'weight' => [
'unitOfMeasure' => 'KG',
'value' => '1',
],
'packedItems' => $this->items,
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class InventoryItem
{
/** @var Quantity */
private $quantity;
/** @var string|null */
private $vendorProductIdentifier;
/** @var bool|null */
private $isObsolete;
/** @var string */
private $buyerProductIdentifier;
public function __construct(
Quantity $quantity,
?string $vendorProductIdentifier = null,
?bool $isObsolete = false,
?string $buyerProductIdentifier = null
) {
$this->quantity = $quantity;
$this->vendorProductIdentifier = $vendorProductIdentifier;
$this->isObsolete = $isObsolete;
$this->buyerProductIdentifier = $buyerProductIdentifier;
}
public function toArray(): array
{
return array_filter(
[
'buyerProductIdentifier' => $this->buyerProductIdentifier,
'vendorProductIdentifier' => $this->vendorProductIdentifier,
'availableQuantity' => $this->quantity->toArray(),
'isObsolete' => $this->isObsolete,
]
);
}
}
@@ -0,0 +1,100 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
use Xentral\Modules\AmazonVendorDF\Exception\MissingInformationException;
class Invoice
{
/** @var string */
private $invoiceNumber;
/** @var DateTime */
private $invoiceDate;
/** @var Address */
private $billToAddress;
/** @var Price */
private $invoiceTotal;
/** @var array|InvoiceItem[] */
private $items;
/** @var SellingParty */
private $remitToParty;
/** @var Warehouse */
private $warehouse;
public function __construct(
string $invoiceNumber,
DateTime $invoiceDate,
SellingParty $remitToParty,
Warehouse $warehouse
) {
$this->invoiceNumber = $invoiceNumber;
$this->invoiceDate = $invoiceDate;
$this->remitToParty = $remitToParty;
$this->warehouse = $warehouse;
}
public function addItem(InvoiceItem $item): self
{
$this->items[] = $item;
return $this;
}
public function setBillToAddress(Address $address)
{
$this->billToAddress = $address;
}
public function setInvoiceTotal(Price $invoiceTotal): self
{
$this->invoiceTotal = $invoiceTotal;
return $this;
}
public function toArray()
{
if (!$this->invoiceTotal) {
throw MissingInformationException::property('invoiceTotal');
}
return [
'invoiceNumber' => $this->invoiceNumber,
'invoiceDate' => $this->invoiceDate,
'remitToParty' => $this->remitToParty->toArray(),
'shipFromParty' => $this->formatShipFromParty(),
'invoiceTotal' => $this->invoiceTotal->toArray(),
'taxTotals' => $this->grabTaxTotalsFromInvoiceItems(),
'items' => $this->mapInvoiceItemsToArray(),
];
}
private function grabTaxTotalsFromInvoiceItems(): array
{
return array_map(function (InvoiceItem $item){
return $item->getTaxDetails()->toArray();
}, $this->items);
}
private function mapInvoiceItemsToArray()
{
return array_map(
function (InvoiceItem $item) {
return $item->toArray();
},
$this->items
);
}
/**
* Currently the warehouse uses the taxRegistrationDetails and address of the remitToParty
*/
private function formatShipFromParty(): array
{
$data = $this->remitToParty->toArray();
$data['partyId'] = $this->warehouse->getWarehouseId();
return $data;
}
}
@@ -0,0 +1,118 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class InvoiceItem
{
/** @var string */
private $itemSequenceNumber;
/** @var string */
private $buyerProductIdentifier;
/** @var string */
private $vendorProductIdentifier;
/** @var Quantity */
private $invoicedQuantity;
/** @var Price */
private $netCost;
/** @var string */
private $purchaseOrderNumber;
/** @var TaxDetails */
private $taxDetails;
public function getItemSequenceNumber(): string
{
return $this->itemSequenceNumber;
}
public function setItemSequenceNumber(string $itemSequenceNumber): self
{
$this->itemSequenceNumber = $itemSequenceNumber;
return $this;
}
public function getBuyerProductIdentifier(): string
{
return $this->buyerProductIdentifier;
}
public function setBuyerProductIdentifier(string $buyerProductIdentifier): self
{
$this->buyerProductIdentifier = $buyerProductIdentifier;
return $this;
}
public function getVendorProductIdentifier(): string
{
return $this->vendorProductIdentifier;
}
public function setVendorProductIdentifier(string $vendorProductIdentifier): self
{
$this->vendorProductIdentifier = $vendorProductIdentifier;
return $this;
}
public function getInvoicedQuantity(): Quantity
{
return $this->invoicedQuantity;
}
public function setInvoicedQuantity(Quantity $invoicedQuantity): self
{
$this->invoicedQuantity = $invoicedQuantity;
return $this;
}
public function getNetCost(): Price
{
return $this->netCost;
}
public function setNetCost(Price $netCost): self
{
$this->netCost = $netCost;
return $this;
}
public function getPurchaseOrderNumber(): string
{
return $this->purchaseOrderNumber;
}
public function setPurchaseOrderNumber(string $purchaseOrderNumber): self
{
$this->purchaseOrderNumber = $purchaseOrderNumber;
return $this;
}
public function getTaxDetails(): TaxDetails
{
return $this->taxDetails;
}
public function setTaxDetails(TaxDetails $taxDetails): self
{
$this->taxDetails = $taxDetails;
return $this;
}
public function toArray()
{
return [
'purchaseOrderNumber' => $this->purchaseOrderNumber,
'itemSequenceNumber' => $this->itemSequenceNumber,
'invoicedQuantity' => $this->invoicedQuantity->toArray(),
'netCost' => $this->netCost->toArray(),
'taxDetails' => $this->taxDetails->toArray(),
// not implemented yet
'chargeDetails' => [],
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class Price
{
/** @var string */
private $currency;
/** @var float */
private $amount;
public function __construct(string $currency, float $amount)
{
$this->currency = $currency;
$this->amount = $amount;
}
public function getCurrency(): string
{
return $this->currency;
}
public function getAmount(): float
{
return $this->amount;
}
public function toArray(): array
{
return [
'currencyCode' => $this->currency,
'amount' => $this->amount,
];
}
}
@@ -0,0 +1,151 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
use InvalidArgumentException;
class PurchaseOrder
{
/** @var string */
private $purchaseOrderNumber;
/** @var DateTime|false */
private $purchaseOrderDate;
/** @var array|PurchaseOrderItem[] */
private $items;
/** @var SellingParty */
private $sellingParty;
/** @var Address */
private $shipToParty;
/** @var string */
private $warehouseId;
/** @var ShipmentDetails */
private $shipmentDetails;
/** @var array */
private $rawData;
public function __construct(
string $purchaseOrderNumber,
$purchaseOrderDate,
array $items,
SellingParty $sellingParty,
string $warehouseId,
Address $shipToParty,
ShipmentDetails $shipmentDetails,
array $rawData = []
) {
$this->purchaseOrderNumber = $purchaseOrderNumber;
$this->purchaseOrderDate = $purchaseOrderDate;
$this->items = $items;
$this->sellingParty = $sellingParty;
$this->warehouseId = $warehouseId;
$this->shipToParty = $shipToParty;
$this->shipmentDetails = $shipmentDetails;
$this->rawData = $rawData;
}
/** @return string */
public function getPurchaseOrderNumber(): string
{
return $this->purchaseOrderNumber;
}
/** @return DateTime|false */
public function getPurchaseOrderDate()
{
return $this->purchaseOrderDate;
}
/** @return array|PurchaseOrderItem[] */
public function getItems()
{
return $this->items;
}
/** @return SellingParty */
public function getSellingParty(): SellingParty
{
return $this->sellingParty;
}
public function getShipToParty(): Address
{
return $this->shipToParty;
}
public function getShipmentDetails(): ShipmentDetails
{
return $this->shipmentDetails;
}
public function getRawData(): array
{
return $this->rawData;
}
public static function fromPurchaseOrderResponse(array $data)
{
if (isset($data['payload'])) {
$data = $data['payload'];
}
$items = array_map(
function (array $item) {
return PurchaseOrderItem::fromPurchaseOrderResponse($item);
},
$data['orderDetails']['items']
);
$sellingParty = new SellingParty($data['orderDetails']['sellingParty']['partyId']);
$shipToParty = (new Address())
->setName($data['orderDetails']['shipToParty']['name'])
->setAddressLines(
[
$data['orderDetails']['shipToParty']['addressLine1'],
$data['orderDetails']['shipToParty']['addressLine2'],
$data['orderDetails']['shipToParty']['addressLine3'],
]
)
->setCity($data['orderDetails']['shipToParty']['city'])
->setStateOrRegion($data['orderDetails']['shipToParty']['stateOrRegion'])
->setPostalCode($data['orderDetails']['shipToParty']['postalCode'])
->setCountryCode($data['orderDetails']['shipToParty']['countryCode']);
$shipmentDetails = new ShipmentDetails(
$data['orderDetails']['shipmentDetails']['isPriorityShipment'],
$data['orderDetails']['shipmentDetails']['isPslipRequired'],
$data['orderDetails']['shipmentDetails']['shipMethod'],
self::parseDate($data['orderDetails']['shipmentDetails']['shipmentDates']['requiredShipDate']),
self::parseDate($data['orderDetails']['shipmentDetails']['shipmentDates']['promisedDeliveryDate']),
isset($data['orderDetails']['shipmentDetails']['messageToCustomer'])
? $data['orderDetails']['shipmentDetails']['messageToCustomer']
: ''
);
// @TODO billToParty needs to be set
return new static(
$data['purchaseOrderNumber'],
self::parseDate($data['orderDetails']['orderDate']),
$items,
$sellingParty,
$data['orderDetails']['shipFromParty']['partyId'],
$shipToParty,
$shipmentDetails,
$data
);
}
protected static function parseDate(string $iso8601DateString): DateTime
{
$date = DateTime::createFromFormat(
DateTime::ISO8601,
$iso8601DateString
);
if(!$date instanceof DateTime){
throw new InvalidArgumentException("Date is not in ISO8601 format: {$iso8601DateString}");
}
return $date;
}
}
@@ -0,0 +1,133 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
use Xentral\Modules\AmazonVendorDF\Exception\MissingInformationException;
class PurchaseOrderAcknowledgement
{
/** @var string */
private $vendorOrderNumber;
/** @var string */
private $purchaseOrderNumber;
/** @var SellingParty */
private $sellingParty;
/** @var Warehouse */
private $warehouse;
/** @var array|AcknowledgementItem[] */
private $items = [];
public function __construct(string $purchaseOrderNumber)
{
$this->purchaseOrderNumber = $purchaseOrderNumber;
}
public function getPurchaseOrderNumber(): string
{
return $this->purchaseOrderNumber;
}
public function addItem(AcknowledgementItem $item): self
{
$this->items[] = $item;
return $this;
}
public function setVendorOrderNumber(string $vendorOrderNumber): self
{
$this->vendorOrderNumber = $vendorOrderNumber;
return $this;
}
public function setSellingParty(SellingParty $sellingParty): self
{
$this->sellingParty = $sellingParty;
return $this;
}
public function setWarehouse(Warehouse $warehouse): self
{
$this->warehouse = $warehouse;
return $this;
}
public function hasRejectedItems(): bool
{
foreach ($this->items as $item) {
if ($item->isRejected()) {
return true;
}
}
return false;
}
protected function getStatusCodeOfFirstRejectedItem(): string
{
foreach ($this->items as $item) {
if ($item->isRejected()) {
return $item->getStatusCode();
}
}
throw new \RuntimeException('No rejected item found');
}
protected function generateStatus(): array
{
$statusCode = AcknowledgementItem::CODE_ACCEPTED;
if ($this->hasRejectedItems()) {
$statusCode = $this->getStatusCodeOfFirstRejectedItem();
}
return [
'code' => $statusCode,
'description' => AcknowledgementItem::AVAILABLE_CODES[$statusCode],
];
}
public function toArray(): array
{
if (!$this->warehouse) {
throw MissingInformationException::property('warehouse');
}
if ($this->warehouse->hasNoAddress()) {
throw MissingInformationException::property('warehouse address');
}
if (!$this->vendorOrderNumber) {
throw MissingInformationException::property('vendorOrderNumber');
}
// Map AcknowledgementItems to array
$items = array_map(
function (AcknowledgementItem $item) {
return $item->toArray();
},
$this->items
);
$data = [
'purchaseOrderNumber' => $this->purchaseOrderNumber,
'vendorOrderNumber' => $this->vendorOrderNumber,
'acknowledgementDate' => (new DateTime())->format(DateTime::ATOM),
'acknowledgementStatus' => $this->generateStatus(),
'sellingParty' => $this->sellingParty->toArray(),
'shipFromParty' => $this->warehouse->toArray(),
'itemAcknowledgements' => $items,
];
// In the PurchaseOrderAcknowledgement endpoint the key
// is named taxInfo instead of taxRegistrationDetails
$data['sellingParty']['taxInfo'] = $data['sellingParty']['taxRegistrationDetails'];
$data['shipFromParty']['taxInfo'] = $data['sellingParty']['taxInfo'];
unset($data['sellingParty']['taxRegistrationDetails']);
unset($data['sellingParty']['taxInfo']['taxRegistrationAddress']);
unset($data['shipFromParty']['taxInfo']['taxRegistrationAddress']);
return $data;
}
}
@@ -0,0 +1,102 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class PurchaseOrderItem
{
/** @var string */
private $itemSequenceNumber;
/** @var string */
private $buyerProductIdentifier;
/** @var string */
private $vendorProductIdentifier;
/** @var string */
private $title;
/** @var Quantity */
private $quantity;
/** @var Price */
private $price;
/** @var float */
private $taxRate;
public function __construct(
string $itemSequenceNumber,
string $buyerProductIdentifier,
string $vendorProductIdentifier,
string $title,
Quantity $quantity,
Price $price,
float $taxRate
) {
$this->itemSequenceNumber = $itemSequenceNumber;
$this->buyerProductIdentifier = $buyerProductIdentifier;
$this->vendorProductIdentifier = $vendorProductIdentifier;
$this->title = $title;
$this->quantity = $quantity;
$this->price = $price;
$this->taxRate = $taxRate;
}
public function getItemSequenceNumber(): string
{
return $this->itemSequenceNumber;
}
public function getBuyerProductIdentifier(): string
{
return $this->buyerProductIdentifier;
}
public function getVendorProductIdentifier(): string
{
return $this->vendorProductIdentifier;
}
public function setVendorProductIdentifier(string $vendorProductIdentifier): void
{
$this->vendorProductIdentifier = $vendorProductIdentifier;
}
public function getQuantity(): Quantity
{
return $this->quantity;
}
public function getPrice(): Price
{
return $this->price;
}
public function getTitle(): string
{
return $this->title;
}
public function getTaxRate(): float
{
return $this->taxRate;
}
public function reject(string $code): AcknowledgementItem
{
return new AcknowledgementItem($this, $code);
}
public function accept(): AcknowledgementItem
{
return new AcknowledgementItem($this, AcknowledgementItem::CODE_ACCEPTED);
}
public static function fromPurchaseOrderResponse(array $data): self
{
return new static(
$data['itemSequenceNumber'],
$data['buyerProductIdentifier'],
$data['vendorProductIdentifier'],
$data['title'],
Quantity::fromArray($data['orderedQuantity']),
new Price($data['netPrice']['currencyCode'], $data['netPrice']['amount']),
(float)$data['taxDetails']['taxLineItem'][0]['taxRate']
);
}
}
@@ -0,0 +1,53 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class Quantity
{
/** @var int */
private $amount;
/** @var string */
private $unitOfMeasure;
/** @var int */
private $unitSize;
public function __construct(int $amount, string $unitOfMeasure = 'Each', ?int $unitSize = 1)
{
$this->amount = $amount;
$this->unitOfMeasure = $unitOfMeasure;
$this->unitSize = $unitSize;
}
public function getAmount(): int
{
return $this->amount;
}
public function getUnitOfMeasure(): string
{
return $this->unitOfMeasure;
}
public function getUnitSize(): int
{
return $this->unitSize;
}
public function toArray(): array
{
return [
'amount' => $this->amount,
'unitOfMeasure' => $this->unitOfMeasure,
'unitSize' => $this->unitSize,
];
}
public static function fromArray(array $data)
{
return new static(
$data['amount'],
$data['unitOfMeasure'],
isset($data['unitSize']) ? $data['unitSize'] : null
);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class SellingParty
{
/** @var string */
private $partyId;
/** @var Address */
private $address;
/** @var TaxRegistrationDetails */
private $taxRegistrationDetails;
public function __construct(string $partyId)
{
$this->partyId = $partyId;
}
public function getPartyId(): string
{
return $this->partyId;
}
public function getAddress(): Address
{
return $this->address;
}
public function setAddress(Address $address): self
{
$this->address = $address;
return $this;
}
public function getTaxRegistrationDetails(): TaxRegistrationDetails
{
return $this->taxRegistrationDetails;
}
public function setTaxRegistrationDetails(TaxRegistrationDetails $taxRegistrationDetails): self
{
$this->taxRegistrationDetails = $taxRegistrationDetails;
return $this;
}
public function toArray(): array
{
return [
'partyId' => $this->partyId,
'address' => $this->address->toArray(),
'taxRegistrationDetails' => $this->taxRegistrationDetails->toArray()
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class ShipmentConfirmation extends ShippingLabelRequest
{
public function toArray()
{
$data = [
'purchaseOrderNumber' => $this->purchaseOrderNumber,
'sellingParty' => $this->sellingParty->toArray(),
'shipmentDetails' => [
'shippedDate' => (new \DateTime('now'))->format(DATE_ATOM),
'shipmentStatus' => 'SHIPPED'
],
'shipFromParty' => $this->warehouse->toArray(),
'items' => $this->extractItemsFromContainers(),
'containers' => array_map(
function (Container $container) {
return $container->toArray();
},
$this->containers
),
];
$data['sellingParty']['taxRegistrationDetails'] = [$data['sellingParty']['taxRegistrationDetails']];
return $data;
}
/**
* Extract all items form the single containers because they are
* needed in the top level of the shipment confirmation as well.
*/
private function extractItemsFromContainers(): array
{
$items = [];
foreach ($this->containers as $container) {
$items = array_merge($items, $container->getItems());
}
return array_map(
function (array $item) {
// In the items of the shipmentConfirmation the key is
// called shippedQuantity instead of packedQuantity
$item['shippedQuantity'] = $item['packedQuantity'];
unset($item['packedQuantity']);
return $item;
},
$items
);
}
}
@@ -0,0 +1,68 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
class ShipmentDetails
{
/** @var bool */
private $isPriorityShipment;
/** @var bool */
private $isPslipRequired;
/** @var string */
private $shipMethod;
/** @var DateTime */
private $promisedDeliveryDate;
/** @var DateTime */
private $requiredShipDate;
/** @var string */
private $messageToCustomer;
public function __construct(
bool $isPriorityShipment,
bool $isPslipRequired,
string $shipMethod,
DateTime $requiredShipDate,
DateTime $promisedDeliveryDate,
string $messageToCustomer
) {
$this->isPriorityShipment = $isPriorityShipment;
$this->isPslipRequired = $isPslipRequired;
$this->shipMethod = $shipMethod;
$this->requiredShipDate = $requiredShipDate;
$this->promisedDeliveryDate = $promisedDeliveryDate;
$this->messageToCustomer = $messageToCustomer;
}
public function isPriorityShipment(): bool
{
return $this->isPriorityShipment;
}
public function isPslipRequired(): bool
{
return $this->isPslipRequired;
}
public function getShipMethod(): string
{
return $this->shipMethod;
}
public function getRequiredShipDate(): DateTime
{
return $this->requiredShipDate;
}
public function getPromisedDeliveryDate(): DateTime
{
return $this->promisedDeliveryDate;
}
public function getMessageToCustomer(): string
{
return $this->messageToCustomer;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class ShippingLabel implements \JsonSerializable
{
/** @var string */
private $purchaseOrderNumber;
/** @var string */
private $encodedLabelData;
/** @var string */
private $labelFormat;
/** @var string */
private $trackingNumber;
public function __construct(string $purchaseOrderNumber, string $encodedLabelData, string $labelFormat = 'PNG')
{
//@TODO check if we need to implement multiple labels per order
$this->purchaseOrderNumber = $purchaseOrderNumber;
$this->encodedLabelData = $encodedLabelData;
$this->labelFormat = $labelFormat;
}
public function getTrackingNumber(): string
{
return $this->trackingNumber;
}
public function setTrackingNumber(string $trackingNumber): self
{
$this->trackingNumber = $trackingNumber;
return $this;
}
public function hasTrackingNumber(): bool
{
return $this->trackingNumber !== null;
}
public function getEncodedLabelData(): string
{
return $this->encodedLabelData;
}
public function jsonSerialize(): array
{
return [
'purchase_order_number' => $this->purchaseOrderNumber,
'tracking_number' => $this->trackingNumber,
'encodedLabelData' => $this->encodedLabelData
];
}
}
@@ -0,0 +1,46 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class ShippingLabelRequest
{
/** @var string */
protected $purchaseOrderNumber;
/** @var SellingParty */
protected $sellingParty;
/** @var Warehouse */
protected $warehouse;
/** @var array|Container[] */
protected $containers = [];
public function __construct(string $purchaseOrderNumber, SellingParty $sellingParty, Warehouse $warehouse)
{
$this->purchaseOrderNumber = $purchaseOrderNumber;
$this->sellingParty = $sellingParty;
$this->warehouse = $warehouse;
}
public function addContainer(Container $container)
{
$this->containers[] = $container;
}
public function toArray()
{
return [
'purchaseOrderNumber' => $this->purchaseOrderNumber,
'sellingParty' => [
'partyId' => $this->sellingParty->getPartyId()
],
'shipFromParty' => [
'partyId' => $this->warehouse->getWarehouseId()
],
'containers' => array_map(
function (Container $container) {
return $container->toArray();
},
$this->containers
),
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class TaxDetails
{
/** @var string */
private $taxType;
/** @var string */
private $taxRate;
/** @var Price */
private $taxAmount;
/** @var Price */
private $taxableAmount;
public function __construct(string $taxType, string $taxRate, Price $taxAmount, Price $taxableAmount)
{
$this->taxType = $taxType;
$this->taxRate = $taxRate;
$this->taxAmount = $taxAmount;
$this->taxableAmount = $taxableAmount;
}
public function toArray()
{
return [
'taxType' => $this->taxType,
'taxRate' => $this->taxRate,
'taxAmount' => $this->taxAmount->toArray(),
'taxableAmount' => $this->taxableAmount->toArray(),
];
}
}
@@ -0,0 +1,58 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class TaxRegistrationDetails
{
/** @var string */
private $taxRegistrationType;
/** @var string */
private $taxRegistrationNumber;
/** @var Address */
private $taxRegistrationAddress;
public function getTaxRegistrationType(): string
{
return $this->taxRegistrationType;
}
public function setTaxRegistrationType(string $taxRegistrationType): self
{
$this->taxRegistrationType = $taxRegistrationType;
return $this;
}
public function getTaxRegistrationNumber(): string
{
return $this->taxRegistrationNumber;
}
public function setTaxRegistrationNumber(string $taxRegistrationNumber): self
{
$this->taxRegistrationNumber = $taxRegistrationNumber;
return $this;
}
public function getTaxRegistrationAddress(): Address
{
return $this->taxRegistrationAddress;
}
public function setTaxRegistrationAddress(Address $taxRegistrationAddress): self
{
$this->taxRegistrationAddress = $taxRegistrationAddress;
return $this;
}
public function toArray(): array
{
return [
'taxRegistrationType' => $this->taxRegistrationType,
'taxRegistrationNumber' => $this->taxRegistrationNumber,
'taxRegistrationAddress' => $this->taxRegistrationAddress->toArray(),
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
use DateTimeImmutable;
use Psr\Http\Message\ResponseInterface;
class Token
{
/** @var string */
private $accessToken;
/** @var string */
private $refreshToken;
/** @var DateTimeImmutable */
private $expirationDate;
public function __construct(string $accessToken, string $refreshToken, int $expiresInSeconds = 3600)
{
$this->accessToken = $accessToken;
$this->refreshToken = $refreshToken;
$this->expirationDate = new DateTimeImmutable( "+{$expiresInSeconds} seconds");
}
public function getAccessToken(): string
{
return $this->accessToken;
}
public function getRefreshToken(): string
{
return $this->refreshToken;
}
public function isExpired(): bool
{
return new DateTime('now') > $this->expirationDate;
}
public static function fromResponse(ResponseInterface $response): self
{
$tokenInformation = json_decode($response->getBody()->getContents(), true);
return new static($tokenInformation['access_token'], $tokenInformation['refresh_token'], $tokenInformation['expires_in']);
}
}
@@ -0,0 +1,153 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
use DateTime;
class Transaction
{
const STATUS_FAILURE = 'Failure';
const STATUS_PROCESSING = 'Processing';
const STATUS_SUCCESS = 'Success';
const STATUS_WAITING = 'Waiting';
const STATUS_CLOSED = 'Closed';
/** @var int */
private $id;
/** @var string */
private $externalId;
/** @var string */
private $subject;
/** @var string */
private $subject_id;
/** @var string */
private $status;
/** @var array */
private $errors;
/** @var DateTime */
private $created_at;
/** @var DateTime */
private $updated_at;
public function __construct(string $subject = '')
{
$this->subject = $subject;
}
public function isWaiting(): bool
{
return $this->status === self::STATUS_WAITING;
}
public function isProcessing(): bool
{
return $this->status === self::STATUS_PROCESSING;
}
public function hasFailed(): bool
{
return $this->status === self::STATUS_FAILURE;
}
public function hasSucceeded(): bool
{
return $this->status === self::STATUS_SUCCESS;
}
public function getId(): ?int
{
return $this->id;
}
public function setId(int $id): self
{
$this->id = $id;
return $this;
}
public function getExternalId(): ?string
{
return $this->externalId;
}
public function setExternalId(string $externalId): self
{
$this->externalId = $externalId;
return $this;
}
public function getSubject(): string
{
return $this->subject;
}
public function setSubject(string $subject): self
{
$this->subject = $subject;
return $this;
}
public function getSubjectId(): ?string
{
return $this->subject_id;
}
public function setSubjectId(string $subject_id): self
{
$this->subject_id = $subject_id;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}
public function getErrors(): array
{
return $this->errors;
}
public function setErrors(array $errors): self
{
$this->errors = $errors;
return $this;
}
public function getCreatedAt(): DateTime
{
return $this->created_at;
}
public function setCreatedAt(DateTime $created_at): self
{
$this->created_at = $created_at;
return $this;
}
public function getUpdatedAt(): DateTime
{
return $this->updated_at;
}
public function setUpdatedAt(DateTime $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Data;
class Warehouse
{
/** @var string */
private $warehouseId;
/** @var Address */
private $address;
public function __construct(string $warehouseId)
{
$this->warehouseId = $warehouseId;
}
public function setAddress(Address $address): self
{
$this->address = $address;
return $this;
}
public function hasNoAddress(): bool
{
return $this->address === null;
}
public function getWarehouseId(): string
{
return $this->warehouseId;
}
public function toArray()
{
return [
'partyId' => $this->warehouseId,
'address' => $this->address->toArray(),
];
}
}
@@ -0,0 +1,8 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
class ColumnNotFoundException extends \RuntimeException
{
}
@@ -0,0 +1,8 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
class DuplicatePurchaseOrderException extends \RuntimeException
{
}
@@ -0,0 +1,26 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
use Exception;
use Xentral\Modules\AmazonVendorDF\Data\AcknowledgementItem;
class InvalidAcknowledgementCodeException extends Exception
{
public static function invalidCode(string $code)
{
return new static(
"Invalid acknowledgement code \"{$code}\". Hast to be one of: \n" . implode(
"\n",
AcknowledgementItem::AVAILABLE_CODES
)
);
}
public static function missingCode(string $code)
{
return new static(
'Acknowledgement is not accepted nor rejected. You have to call accept() or reject()'
);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
class IssueTokenException extends RuntimeException
{
public static function fromResponse(?ResponseInterface $response = null): self
{
return new self($response ? 'exception with response info' : 'exception without response info');
}
}
@@ -0,0 +1,13 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
use Exception;
class MissingInformationException extends Exception
{
public static function property(string $property)
{
return new static("\"{$property}\" is not set!");
}
}
@@ -0,0 +1,8 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
class PurchaseOrderNumberNotFoundException extends \RuntimeException
{
}
@@ -0,0 +1,8 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Exception;
class TransferException extends \RuntimeException
{
}
@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace Xentral\Modules\AmazonVendorDF\Models;
use DateTime;
use Xentral\Modules\AmazonVendorDF\Data\PurchaseOrder;
use Xentral\Modules\AmazonVendorDF\Data\ShippingLabel;
class PurchaseOrderInformation
{
/** Step 1: Purchase order resulted in an entry in the database which was not processed yet */
public const STATUS_UNPROCESSED = null;
/** Step 2: Purchase order resulted in an order but was not acknowledged yet */
public const STATUS_PROCESSING = 'processing';
/** Step 3: Acknowledgement was sent, but it was not yet cleared by Amazon */
public const STATUS_ACKNOWLEDGEMENT_SENT = 'acknowledgement_sent';
/** Step 3b: Purchase could not be processed automatically and requires user input */
public const STATUS_WAITING_FOR_USER_INPUT = 'waiting_for_user_input';
/** Step 3c: There was an error on the remote end preventing the acknowledgement from being processed*/
public const STATUS_ACKNOWLEDGEMENT_FAILED = 'acknowledgement_failed';
/** Step 4: Acknowledgement was accepted by Amazon */
public const STATUS_ACKNOWLEDGEMENT_ACCEPTED = 'acknowledgement_accepted';
/** Step 4b: Acknowledgement was rejected by amazon */
public const STATUS_ACKNOWLEDGEMENT_REJECTED = 'acknowledgement_rejected';
/** Step 5: Shipping label was requested, but not yet provided by Amazon */
public const STATUS_SHIPPING_LABEL_REQUESTED = 'shipping_label_requested';
/** Step 6: Shipping label request was successful and can be downloaded */
public const STATUS_SHIPPING_LABEL_ACCEPTED = 'shipping_label_accepted';
/** Step 6b: Shipping label request was rejected by amazon */
public const STATUS_SHIPPING_LABEL_REJECTED = 'shipping_label_rejected';
/** Step 7: */
public const STATUS_SHIPMENT_CONFIRMATION_SENT = 'shipment_confirmation_sent';
/** Rejected by user input */
public const STATUS_REJECTED_BY_USER_INPUT = 'rejected_by_user_input';
/** @var string */
private $externalId;
/** @var string */
private $raw;
/** @var null|int */
private $orderId;
/** @var bool */
private $acknowledged = false;
/** @var null|string */
private $acknowledgementTransactionId;
/** @var bool */
private $shippingLabelRequested = false;
/** @var null|string */
private $shippingLabelRequestTransactionId;
/** @var null|string */
private $shippingLabelData;
/** @var DateTime|null */
private $createdAt;
/** @var Datetime|null */
private $updatedAt;
/** @var string */
private $status;
/** @var string */
private $shipmentConfirmationTransactionId;
public function __construct(string $externalId)
{
$this->externalId = $externalId;
}
/**
* @return null|string
*/
public function getShipmentConfirmationTransactionId(): ?string
{
return $this->shipmentConfirmationTransactionId;
}
/**
* @param string $shipmentConfirmationTransactionId
*/
public function setShipmentConfirmationTransactionId(string $shipmentConfirmationTransactionId): void
{
$this->shipmentConfirmationTransactionId = $shipmentConfirmationTransactionId;
}
/**
* @return bool
*/
public function wasShipmentConfirmationSent(): bool
{
return $this->shipmentConfirmationTransactionId !== null;
}
/**
* @return string
*/
public function getStatus(): ?string
{
return $this->status;
}
/**
* @param string $status
*/
public function setStatus(string $status): void
{
$this->status = $status;
}
public function canFetchShippingLabels(): bool
{
return $this->status === self::STATUS_SHIPPING_LABEL_ACCEPTED
|| (empty($this->shippingLabelData) && $this->status === self::STATUS_SHIPMENT_CONFIRMATION_SENT);
}
public function hasShippingLabel(): bool
{
return $this->shippingLabelData !== null;
}
public function getPurchaseOrderNumber(): string
{
return $this->externalId;
}
public function getRawJson(): ?string
{
return $this->raw;
}
public function getOrderId(): ?int
{
return $this->orderId;
}
/**
* @param int|null $orderId
*/
public function setOrderId(?int $orderId): void
{
$this->orderId = $orderId;
}
public function isAcknowledged(): bool
{
return $this->acknowledged;
}
/**
* @param bool $acknowledged
*/
public function setAcknowledged(bool $acknowledged): void
{
$this->acknowledged = $acknowledged;
}
public function getAcknowledgementTransactionId(): ?string
{
return $this->acknowledgementTransactionId;
}
/**
* @param string|null $acknowledgementTransactionId
*/
public function setAcknowledgementTransactionId(?string $acknowledgementTransactionId): void
{
$this->acknowledgementTransactionId = $acknowledgementTransactionId;
}
public function isShippingLabelRequested(): bool
{
return $this->shippingLabelRequested;
}
/**
* @param bool $shippingLabelRequested
*/
public function setShippingLabelRequested(bool $shippingLabelRequested): void
{
$this->shippingLabelRequested = $shippingLabelRequested;
}
/**
* @return string
*/
public function getExternalId(): string
{
return $this->externalId;
}
/**
* @return string|null
*/
public function getShippingLabelRequestTransactionId(): ?string
{
return $this->shippingLabelRequestTransactionId;
}
/**
* @param string|null $shippingLabelRequestTransactionId
*/
public function setShippingLabelRequestTransactionId(?string $shippingLabelRequestTransactionId): void
{
$this->shippingLabelRequestTransactionId = $shippingLabelRequestTransactionId;
}
/**
* @return ShippingLabel[]
*/
public function getShippingLabels(): array
{
$decodedShippingLabels = json_decode($this->shippingLabelData, true);
return array_map(
function (array $data) {
$shippingLabel = new ShippingLabel(
$data['purchase_order_number'],
$data['encodedLabelData']
);
$shippingLabel->setTrackingNumber($data['tracking_number']);
return $shippingLabel;
},
$decodedShippingLabels
);
}
public function setShippingLabels(array $shippingLabels): void
{
$this->shippingLabelData = json_encode($shippingLabels);
}
public function getShippingLabelData(): ?string
{
return $this->shippingLabelData;
}
public function setShippingLabelData(string $shippingLabelData): void
{
$this->shippingLabelData = $shippingLabelData;
}
public function setRaw(string $raw): void
{
$this->raw = $raw;
}
public function getPurchaseOrder(): PurchaseOrder
{
return PurchaseOrder::fromPurchaseOrderResponse(json_decode($this->raw, true));
}
public function getCreatedAt(): ?DateTime
{
return $this->createdAt;
}
/**
* @param DateTime $createdAt
*/
public function setCreatedAt(DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
public function getUpdatedAt(): ?DateTime
{
return $this->updatedAt;
}
/**
* @param Datetime $updatedAt
*/
public function setUpdatedAt(Datetime $updatedAt): void
{
$this->updatedAt = $updatedAt;
}
}
@@ -0,0 +1,267 @@
<?php
namespace Xentral\Modules\AmazonVendorDF;
use Xentral\Components\Database\Database;
use Xentral\Modules\AmazonVendorDF\Data\PurchaseOrder;
use Xentral\Modules\AmazonVendorDF\Models\PurchaseOrderInformation;
use Xentral\Modules\AmazonVendorDF\Exception\ColumnNotFoundException;
use Xentral\Modules\AmazonVendorDF\Exception\DuplicatePurchaseOrderException;
use Xentral\Modules\AmazonVendorDF\Exception\PurchaseOrderNumberNotFoundException;
class PurchaseOrderInformationRepository
{
/** @var string */
private $tableName = 'amazon_vendor_df_purchase_orders';
private $columns = [
'status',
'external_id',
'raw',
'order_id',
'acknowledged',
'acknowledgement_transaction_id',
'shipping_label_requested',
'shipping_label_request_transaction_id',
'shipping_label_data',
'shipment_confirmation_transaction_id',
'created_at',
'updated_at',
'shopexport_id',
];
/** @var Database */
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function createPurchaseOrderInformation(PurchaseOrder $purchaseOrder, int $shopExportId):void
{
if ($this->doesPurchaseOrderInformationExist($purchaseOrder->getPurchaseOrderNumber())) {
throw new DuplicatePurchaseOrderException();
}
$statement = $this->database
->insert()
->into($this->tableName)
->cols(['external_id', 'raw', 'shopexport_id'])
->getStatement();
$values = [
'external_id' => $purchaseOrder->getPurchaseOrderNumber(),
'raw' => json_encode($purchaseOrder->getRawData()),
'shopexport_id' => json_encode($shopExportId),
];
$this->database->perform($statement, $values);
}
public function doesPurchaseOrderInformationExist(string $purchaseOrderNumber): bool
{
$statement = $this->database
->select()
->cols(['id'])
->from($this->tableName)
->where('external_id = :external_id')
->bindValue('external_id', $purchaseOrderNumber)
->limit(1);
$purchaseOrderId = $this->database->fetchRow($statement->getStatement(), $statement->getBindValues());
return !empty($purchaseOrderId['id']);
}
public function countPurchaseOrdersWaitingForImport(int $shopExportId): int
{
$statement = $this->database
->select()
->cols(['id'])
->from($this->tableName)
->where('shopexport_id = :shopexport_id')
->where('acknowledged = 0')
->where('status IS NULL')
->bindValue('shopexport_id', $shopExportId);
return count($this->database->fetchAssoc($statement->getStatement(), $statement->getBindValues()));
}
public function getNextPurchaseOrderInformationToImport(int $shopExportId): PurchaseOrderInformation
{
$statement = $this->database
->select()
->cols($this->columns)
->from($this->tableName)
->where('shopexport_id = :shopexport_id')
->where('acknowledged = 0')
->where('status IS NULL') //TODO Konstante verwenden
->bindValue('shopexport_id', $shopExportId)
->orderBy(['created_at ASC'])
->limit(1);
$data = $this->database->fetchRow($statement->getStatement(), $statement->getBindValues());
return $this->buildPurchaseOrderInformation($data);
}
/**
* @param array $constraints
*
* @return PurchaseOrderInformation[]
*/
public function listPurchaseOrderInformation(array $constraints = []): array
{
$statement = $this->database
->select()
->cols($this->columns)
->from($this->tableName);
foreach ($constraints as $constraint) {
$column = $constraint[0];
if(!in_array($column,$this->columns, false)){
throw new ColumnNotFoundException("Column '{$column}' does not exist in table {$this->tableName}");
}
$value = $constraint[1];
if ($value === null) {
$statement->where("{$column} IS NULL");
continue;
}
$operator = empty($constraint[2]) ? '=' : $constraint[2];
if ($constraint[1] instanceof \DateTime) {
$statement->where("{$column} {$operator} :{$column}");
$statement->bindValue($column, $value->format('Y-m-d H:i:s'));
} else {
$statement->where("{$column} {$operator} :{$column}");
$statement->bindValue($column, $value);
}
}
return array_map(
function (array $data) {
return $this->buildPurchaseOrderInformation($data);
},
$this->database->fetchAll($statement->getStatement(), $statement->getBindValues())
);
}
public function savePurchaseOrderInformation(PurchaseOrderInformation $purchaseOrderInformation): PurchaseOrderInformation
{
if (empty($purchaseOrderInformation->getPurchaseOrderNumber())) {
//TODO Throw
}
$columnsToSave = [];
$columnsToIgnore = ['raw', 'created_at', 'updated_at', 'shopexport_id'];
foreach ($this->columns as $column){
if(!in_array($column,$columnsToIgnore)){
$columnsToSave[] = $column;
}
}
$statement = $this->database
->update()
->cols($columnsToSave)
->table($this->tableName)
->where('external_id = :external_id')
->bindValue('external_id', $purchaseOrderInformation->getPurchaseOrderNumber())
->bindValues(
[
'order_id' => $purchaseOrderInformation->getOrderId(),
'status' => $purchaseOrderInformation->getStatus(),
'acknowledged' => $purchaseOrderInformation->isAcknowledged(),
'acknowledgement_transaction_id' => $purchaseOrderInformation->getAcknowledgementTransactionId(),
'shipping_label_requested' => $purchaseOrderInformation->isShippingLabelRequested(),
'shipping_label_request_transaction_id' => $purchaseOrderInformation->getShippingLabelRequestTransactionId(),
'shipping_label_data' => $purchaseOrderInformation->getShippingLabelData(),
'shipment_confirmation_transaction_id' => $purchaseOrderInformation->getShipmentConfirmationTransactionId(),
]
);
$this->database->perform($statement, $statement->getBindValues());
$purchaseOrderInformation->setUpdatedAt(new \DateTime());
return $purchaseOrderInformation;
}
public function getPurchaseOrderInformationByPurchaseOrderNumber(string $purchaseOrderNumber): PurchaseOrderInformation
{
$statement = $this->database
->select()
->cols($this->columns)
->from($this->tableName)
->where('external_id = :external_id')
->bindValue('external_id', $purchaseOrderNumber)
->limit(1);
$purchaseOrderData = $this->database->fetchRow($statement->getStatement(), $statement->getBindValues());
return $this->buildPurchaseOrderInformation($purchaseOrderData);
}
public function getPurchaseOrderInformationByOrderId(int $orderId): PurchaseOrderInformation
{
$statement = $this->database
->select()
->cols($this->columns)
->from($this->tableName)
->where('order_id = :order_id')
->bindValue('order_id', $orderId)
->limit(1);
$purchaseOrderData = $this->database->fetchRow($statement->getStatement(), $statement->getBindValues());
return $this->buildPurchaseOrderInformation($purchaseOrderData);
}
private function buildPurchaseOrderInformation(array $data): PurchaseOrderInformation
{
if(empty($data['external_id'])){
throw new PurchaseOrderNumberNotFoundException();
}
$purchaseOrderStatus = new PurchaseOrderInformation($data['external_id']);
if (isset($data['raw'])) {
$purchaseOrderStatus->setRaw($data['raw']);
}
if (isset($data['order_id'])) {
$purchaseOrderStatus->setOrderId($data['order_id']);
}
if (isset($data['acknowledged'])) {
$purchaseOrderStatus->setAcknowledged((bool)$data['acknowledged']);
}
if (isset($data['acknowledgement_transaction_id'])) {
$purchaseOrderStatus->setAcknowledgementTransactionId($data['acknowledgement_transaction_id']);
}
if (isset($data['shipping_label_requested'])) {
$purchaseOrderStatus->setShippingLabelRequested((bool)$data['shipping_label_requested']);
}
if (isset($data['shipping_label_request_transaction_id'])) {
$purchaseOrderStatus->setShippingLabelRequestTransactionId($data['shipping_label_request_transaction_id']);
}
if (isset($data['shipping_label_data'])) {
$purchaseOrderStatus->setShippingLabelData($data['shipping_label_data']);
}
if (isset($data['shipment_confirmation_transaction_id'])) {
$purchaseOrderStatus->setAcknowledged($data['shipment_confirmation_transaction_id']);
}
if (isset($data['created_at'])) {
$purchaseOrderStatus->setCreatedAt(\DateTime::createFromFormat('Y-m-d H:i:s', $data['created_at']));
}
if (isset($data['updated_at'])) {
$purchaseOrderStatus->setUpdatedAt(\DateTime::createFromFormat('Y-m-d H:i:s', $data['updated_at']));
}
if (isset($data['status'])) {
$purchaseOrderStatus->setStatus($data['status']);
}
return $purchaseOrderStatus;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use GuzzleHttp\ClientInterface;
use Xentral\Modules\AmazonVendorDF\Data\InventoryItem;
use Xentral\Modules\AmazonVendorDF\Data\Transaction;
class InventoryService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function updateInventory(string $warehouseId, array $items, string $sellingPartyId, bool $isFullUpdate = false)
{
//First map all InventoryItems to an array
$items = array_map(
function (InventoryItem $item) {
$data = $item->toArray();
unset($data['availableQuantity']['unitSize']);
return $data;
},
$items
);
$response = $this->client->request(
'POST',
"/vendor/directFulfillment/inventory/v1/warehouses/{$warehouseId}/items",
[
'json' => [
'inventory' => [
'sellingParty' => [
'partyId' => $sellingPartyId
],
'items' => $items,
'isFullUpdate' => $isFullUpdate
]
],
]
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return (new Transaction('inventory_update'))->setExternalId($payload['transactionId']);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use GuzzleHttp\ClientInterface;
use Xentral\Modules\AmazonVendorDF\Data\Invoice;
use Xentral\Modules\AmazonVendorDF\Data\Transaction;
class InvoiceService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function submitInvoice(Invoice $invoice): Transaction
{
$response = $this->client->request(
'POST',
'/vendor/directFulfillment/payments/v1/invoices',
['json' => [$invoice->toArray()]]
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return (new Transaction('invoice'))->setExternalId($payload['transactionId']);
}
}
@@ -0,0 +1,164 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use DateTime;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\TransferException as GuzzleTransferException;
use Xentral\Modules\AmazonVendorDF\Data\PurchaseOrder;
use Xentral\Modules\AmazonVendorDF\Data\PurchaseOrderAcknowledgement;
use Xentral\Modules\AmazonVendorDF\Data\Transaction;
use Xentral\Modules\AmazonVendorDF\Exception\TransferException;
class PurchaseOrderService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
/**
* @param DateTime|null $createdAfter
* @param DateTime|null $createdBefore
* @param int|null $limit
*
* @return array|string[]
*/
public function getPurchaseOrderNumbers(
?DateTime $createdAfter = null,
?DateTime $createdBefore = null,
?int $limit = null
): array {
$orders = $this->getOrders($createdAfter, $createdBefore, $limit);
return array_map(
function (array $order) {
return $order['purchaseOrderNumber'];
},
$orders
);
}
/**
* @param DateTime|null $createdAfter
* @param DateTime|null $createdBefore
* @param int|null $limit
*
* @return array|PurchaseOrder[]
*/
public function getPurchaseOrders(
?DateTime $createdAfter = null,
?DateTime $createdBefore = null,
?int $limit = null
): array {
$orders = $this->getOrders($createdAfter, $createdBefore, $limit);
return array_map(
function (array $order) {
return PurchaseOrder::fromPurchaseOrderResponse($order);
},
$orders
);
}
/**
* @param DateTime|null $createdAfter
* @param DateTime|null $createdBefore
* @param int|null $limit
*
* @return array
*/
protected function getOrders(
?DateTime $createdAfter = null,
?DateTime $createdBefore = null,
?int $limit = null
): array {
$finished = false;
$nextToken = null;
$orders = [];
if ($limit !== null) {
$response = $this->sendGetOrdersRequest($createdAfter, $createdBefore, null, $limit);
$orders = array_merge($orders, $response['payload']['orders']);
} else {
while (!$finished) {
$response = $this->sendGetOrdersRequest($createdAfter, $createdBefore, $nextToken);
if ($response['payload']['pagination']['nextToken']) {
$nextToken = $response['payload']['pagination']['nextToken'];
} else {
$finished = true;
}
$orders = array_merge($orders, $response['payload']['orders']);
}
}
return $orders;
}
public function getOrder(string $purchaseOrderNumber): PurchaseOrder
{
$response = $this->client->request(
'GET',
"/vendor/directFulfillment/orders/v1/purchaseOrders/{$purchaseOrderNumber}"
);
$payload = json_decode($response->getBody()->getContents(), true);
return PurchaseOrder::fromPurchaseOrderResponse($payload);
}
public function submitAcknowledgement(PurchaseOrderAcknowledgement $acknowledgement): Transaction
{
try {
$response = $this->client->request(
'POST',
'/vendor/directFulfillment/orders/v1/acknowledgements',
[
'json' => [
'orderAcknowledgements' => [$acknowledgement->toArray()],
],
]
);
}catch (GuzzleTransferException $exception){
throw new TransferException('Error while submitting acknowledgement',0, $exception);
}
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return (new Transaction('purchase_order_acknowledgement'))->setExternalId($payload['transactionId']);
}
protected function formatDate(DateTime $date)
{
return $date->format(DateTime::ISO8601);
}
protected function sendGetOrdersRequest(
?DateTime $createdAfter,
?DateTime $createdBefore,
?string $nextToken = null,
?int $limit = null
): array {
$response = $this->client->request(
'GET',
'/vendor/directFulfillment/orders/v1/purchaseOrders',
[
'query' => array_merge(
[
'createdAfter' => $this->formatDate($createdAfter ?? new DateTime('-7 days')),
'createdBefore' => $this->formatDate($createdBefore ?? new DateTime()),
'limit' => $limit ?: 100,
'includeDetails' => true,
'sortOrder' => 'DESC',
],
$nextToken !== null ? ['nextToken' => $nextToken] : []
),
]
);
return json_decode($response->getBody()->getContents(), true);
}
}
@@ -0,0 +1,86 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use GuzzleHttp\ClientInterface;
use Xentral\Modules\AmazonVendorDF\Data\ShipmentConfirmation;
use Xentral\Modules\AmazonVendorDF\Data\ShippingLabel;
use Xentral\Modules\AmazonVendorDF\Data\ShippingLabelRequest;
use Xentral\Modules\AmazonVendorDF\Data\Transaction;
class ShippingService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
/**
* @param string $purchaseOrderNumber
*
* @throws \GuzzleHttp\Exception\GuzzleException
* @return array|ShippingLabel[]
*/
public function getShippingLabels(string $purchaseOrderNumber): array
{
$response = $this->client->request(
'GET',
"/vendor/directFulfillment/shipping/v1/shippingLabels/{$purchaseOrderNumber}"
);
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return array_map(
function (array $data) use ($payload) {
$label = new ShippingLabel($payload['purchaseOrderNumber'], $data['content'], $payload['labelFormat']);
if (isset($data['trackingNumber']) && $data['trackingNumber'] !== '') {
$label->setTrackingNumber($data['trackingNumber']);
}
return $label;
},
$payload['labelData']
);
}
public function submitShippingLabelRequest(ShippingLabelRequest $shippingLabelRequest): Transaction
{
$response = $this->client->request(
'POST',
'/vendor/directFulfillment/shipping/v1/shippingLabels',
[
'json' => [
'shippingLabelRequests' => [$shippingLabelRequest->toArray()],
],
]
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return (new Transaction('shipping_label_request'))->setExternalId($payload['transactionId']);
}
public function submitShipmentConfirmation(ShipmentConfirmation $confirmation)
{
$response = $this->client->request(
'POST',
'/vendor/directFulfillment/shipping/v1/shipmentConfirmations',
[
'json' => [
'shipmentConfirmations' => [
$confirmation->toArray(),
],
],
]
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload'];
return (new Transaction('shipment_confirmation'))->setExternalId($payload['transactionId']);
}
}
@@ -0,0 +1,41 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\BadResponseException;
use Xentral\Modules\AmazonVendorDF\Data\Token;
use Xentral\Modules\AmazonVendorDF\Exception\IssueTokenException;
class TokenService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function requestToken(string $refreshToken, string $clientId, string $clientSecret)
{
try {
$response = $this->client->request(
'POST',
'https://api.amazon.com/auth/o2/token',
[
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $clientId,
'client_secret' => $clientSecret,
],
]
);
}catch (BadResponseException $badResponseException){
throw IssueTokenException::fromResponse($badResponseException->getResponse());
}
return Token::fromResponse($response);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Xentral\Modules\AmazonVendorDF\Service;
use GuzzleHttp\ClientInterface;
use Xentral\Modules\AmazonVendorDF\Data\Transaction;
class TransactionService
{
/** @var ClientInterface */
private $client;
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
public function updateTransactionStatus(Transaction $transaction): Transaction
{
$response = $this->client->request(
'GET',
"/vendor/directFulfillment/transactions/v1/transactions/{$transaction->getExternalId()}"
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload']['transactionStatus'];
$transaction->setStatus($payload['status']);
if($transaction->hasFailed()){
$transaction->setErrors($payload['errors']);
}
return $transaction;
}
public function getTransactionByTransactionId(string $transactionId): Transaction
{
$response = $this->client->request(
'GET',
"/vendor/directFulfillment/transactions/v1/transactions/{$transactionId}"
);
// The response data is wrapped in a `payload` key
$payload = json_decode($response->getBody()->getContents(), true)['payload']['transactionStatus'];
$transaction = new Transaction();
$transaction->setStatus($payload['status']);
if ($transaction->hasFailed()) {
$transaction->setErrors($payload['errors']);
}
return $transaction;
}
}
@@ -0,0 +1,172 @@
<?php
namespace Xentral\Modules\AmazonVendorDF;
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Xentral\Modules\AmazonVendorDF\Data\Token;
use Xentral\Modules\AmazonVendorDF\Service\InventoryService;
use Xentral\Modules\AmazonVendorDF\Service\InvoiceService;
use Xentral\Modules\AmazonVendorDF\Service\PurchaseOrderService;
use Xentral\Modules\AmazonVendorDF\Service\ShippingService;
use Xentral\Modules\AmazonVendorDF\Service\TokenService;
use Xentral\Modules\AmazonVendorDF\Service\TransactionService;
class ServiceFactory
{
const API_BASE_URL = 'https://sellingpartnerapi-eu.amazon.com';
/** @var TokenService */
private $tokenService;
/** @var Token */
private $token;
/** @var string */
private $refreshToken;
/** @var string */
private $clientId;
/** @var string */
private $clientSecret;
/** @var SignatureV4 */
private $signature;
/** @var Credentials */
private $credentials;
/** @var ClientInterface */
private $authenticatedClient;
/** @var LoggerInterface */
private $logger;
public function __construct(
string $refreshToken,
string $clientId,
string $clientSecret,
string $awsIamKey,
string $awsIamSecret,
LoggerInterface $logger
) {
$this->tokenService = new TokenService(new Client());
$this->refreshToken = $refreshToken;
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->signature = new SignatureV4('execute-api', 'eu-west-1');
$this->credentials = new Credentials($awsIamKey, $awsIamSecret);
$this->logger = $logger;
}
public function getShippingService(): ShippingService
{
return new ShippingService($this->getAuthenticatedClient());
}
public function getInvoiceService(): InvoiceService
{
return new InvoiceService($this->getAuthenticatedClient());
}
public function getInventoryService(): InventoryService
{
return new InventoryService($this->getAuthenticatedClient());
}
public function getPurchaseOrderService(): PurchaseOrderService
{
return new PurchaseOrderService($this->getAuthenticatedClient());
}
public function getTransactionService(): TransactionService
{
return new TransactionService($this->getAuthenticatedClient());
}
private function getAuthenticatedClient(): ClientInterface
{
if (!$this->authenticatedClient || $this->getToken()->isExpired()) {
$stack = HandlerStack::create();
$stack->push($this->getSignatureMiddleware());
$stack->push($this->getLoggingMiddleWare());
$this->authenticatedClient = new Client(
[
'handler' => $stack,
'base_uri' => self::API_BASE_URL,
'headers' => [
'x-amz-access-token' => $this->getToken()->getAccessToken(),
],
]
);
}
return $this->authenticatedClient;
}
private function getSignatureMiddleware()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
return $handler($this->signature->signRequest($request, $this->credentials), $options);
};
};
}
private function getLoggingMiddleWare()
{
return function (callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
$promise = $handler($request, $options);
return $promise->then(
function (ResponseInterface $response) use ($request) {
$this->logRequestAndResponse($request, $response, LogLevel::DEBUG);
return $response;
},
function (ResponseInterface $response) use ($request) {
$this->logRequestAndResponse($request, $response, LogLevel::ERROR);
return $response;
}
);
};
};
}
private function logRequestAndResponse(RequestInterface $request, ResponseInterface $response, string $level)
{
$request->getBody()->rewind();
$this->logger->log(
$level,
'Amazon Vendor DF API request',
[
'request' => [
'uri' => (string)$request->getUri(),
'method' => $request->getMethod(),
'body' => json_decode($request->getBody()->getContents(), true),
],
'response' => [
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => json_decode($response->getBody()->getContents(), true),
],
]
);
$response->getBody()->rewind();
}
private function getToken()
{
if (!$this->token) {
$this->token = $this->tokenService->requestToken($this->refreshToken, $this->clientId, $this->clientSecret);
}
return $this->token;
}
}