<?php
namespace App\Service;
use App\Entity\Sale;
use App\Repository\SaleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Picqer\Financials\Exact\SalesOrder;
use Picqer\Financials\Exact\SalesOrderLine;
use Symfony\Component\Console\Style\SymfonyStyle;
class ExactApi
{
private $em;
private $sr;
private $geo;
private $exact;
public function __construct(EntityManagerInterface $em, SaleRepository $sr, Geocoding $geo, Exact $exact)
{
$this->em = $em;
$this->sr = $sr;
$this->geo = $geo;
$this->exact = $exact;
}
/**
* @param $from_date
* @return SalesOrder[]
* @throws \Exception
*/
public function getOrders($from_date)
{
try {
return $this->exact->getOrders($from_date);
} catch (\Exception $exception) {
throw new \Exception($exception->getMessage());
}
}
/**
* @param $order_number
* @return SalesOrder|null
* @throws \Exception
*/
public function getOrder($order_number)
{
try {
return $this->exact->getOrder($order_number);
} catch (\Exception $exception) {
throw new \Exception($exception->getMessage());
}
}
public function sync($from_date = null, SymfonyStyle $io): void
{
try {
if ($this->exact->doRefetchToken()) {
$io->write('Exact requires a new refresh token');
return;
}
if ($orders = $this->getOrders($from_date)) {
$io->progressStart(\count($orders));
foreach ($orders as $order) {
try {
$attr = $order->attributes();
$sale = $this->sr->get($attr['OrderNumber']);
if ($sale->getId()) {
$this->update($order, $sale);
} else {
$this->create($order, $sale);
}
$io->progressAdvance(1);
} catch (\Exception $e) {
continue;
}
}
$io->progressFinish();
}
} catch (\Exception $e) {
if (false !== stripos($e->getMessage(), 'Could not acquire or refresh tokens')) {
$this->exact->setRefetchToken();
} else {
// send email???
}
throw new \Exception('error syncing Exact: ' . $e->getMessage());
}
}
public function syncOrder($id, SymfonyStyle $io): void
{
if ($this->exact->doRefetchToken()) {
$io->write('Exact requires a new refresh token');
return;
}
if ($order = $this->getOrder($id)) {
$attr = $order->attributes();
$sale = $this->sr->get($attr['OrderNumber']);
if ($sale->getId()) {
$this->update($order, $sale);
$io->success('Sync updated order ' . $id);
} else {
$this->create($order, $sale);
$io->success('Sync imported order ' . $id);
}
} else {
$io->error('Order ' . $id . ' does not exist');
}
}
private function create(SalesOrder $order, Sale $sale): void
{
$attr = $order->attributes();
$ba = $this->exact->getAccount($attr['InvoiceTo']);
[$ba_lat, $ba_lng] = $this->geo->getLatLng($ba['AddressLine1'], '', $ba['Postcode'], $ba['City'], $ba['CountryName']);
$sa = $this->exact->getAccount($attr['DeliverTo']);
[$sa_lat, $sa_lng] = $this->geo->getLatLng($sa['AddressLine1'], '', $sa['Postcode'], $sa['City'], $sa['CountryName']);
if ($attr['InvoiceToContactPerson']) {
$contact_invoice = $this->exact->getContact($attr['InvoiceToContactPerson']);
} else {
$contact_invoice = $ba;
}
if ($attr['DeliverToContactPerson']) {
$contact_deliver = $this->exact->getContact($attr['DeliverToContactPerson']);
} else {
$contact_deliver = $sa;
}
[$items, $notes] = $this->getItemsFromOrder($order);
$info = [
'order_id' => $attr['OrderNumber'],
'increment_id' => $attr['OrderNumber'],
'subtotal' => 0,//(float)$order['subtotal_incl_tax'],
'shipping' => 0,//(float)$order['shipping_incl_tax'],
'total' => (float)$attr['AmountFC'],
'tax' => 0,//(float)$order['tax_amount'],
'total_paid' => (float)$attr['AmountFC'],
'total_due' => 0,//(float)$order['total_due'],
'discount' => $this->getDiscount($attr, $items),
'payment_method' => $this->getPaymentMethod($attr['PaymentCondition']),
'shipping_method' => $attr['ShippingMethodDescription'],
'description' => $notes ? trim(implode("\n\n", $notes)) : '',
'address' => [
'billing' => [
'firstname' => '',
'lastname' => $attr['InvoiceToName'] ?: '',
'email' => $this->getEmail($contact_invoice),
'telephone' => $this->getPhone($contact_invoice),
'street' => $ba['AddressLine1'],
'number' => '',
'toevoeging' => '',
'postcode' => $ba['Postcode'],
'city' => $ba['City'],
'country' => $ba['CountryName'],
'lat' => $ba_lat,
'lng' => $ba_lng,
],
'shipping' => [
'firstname' => '',
'lastname' => $attr['DeliverToName'] ?: '',
'email' => $this->getEmail($contact_deliver),
'telephone' => $this->getPhone($contact_deliver),
'street' => $sa['AddressLine1'],
'number' => '',
'toevoeging' => '',
'postcode' => $sa['Postcode'],
'city' => $sa['City'],
'country' => $sa['CountryName'],
'lat' => $sa_lat,
'lng' => $sa_lng,
]
],
'company' => false,
'items' => $items,
];
$sale->setType(Sale::TYPE_EXACT);
$sale->setStatus(Sale::STATUS_PROCESSING);
$sale->setInfo($info);
$sale->setStore(Sale::STORE_VDGARDE);
$sale->setPickup(false);
$sale->setCreatedAt($this->parseDate($attr['Created']));
$this->em->persist($sale);
$this->em->flush();
sleep(1);
}
private function getPhone(array $c)
{
if (isset($c['Mobile']) && $c['Mobile']) {
return $c['Mobile'];
}
if (isset($c['BusinessMobile']) && $c['BusinessMobile']) {
return $c['BusinessMobile'];
}
if (isset($c['Phone']) && $c['Phone']) {
return $c['Phone'];
}
if (isset($c['BusinessPhone']) && $c['BusinessPhone']) {
return $c['BusinessPhone'];
}
return '';
}
private function getEmail(array $c)
{
if (isset($c['Email']) && $c['Email']) {
return $c['Email'];
}
if (isset($c['BusinessEmail']) && $c['BusinessEmail']) {
return $c['BusinessEmail'];
}
return '';
}
private function parseDate($date_str)
{
$dt = (int)str_replace(['/Date(', ')/'], '', $date_str);
return new \DateTime(date('Y-m-d H:i:s', $dt / 1000));
}
private function getPaymentMethod($method)
{
switch ((string)$method) {
case '00': return 'cashondelivery'; //'Contant / Pin bij aflevering'
case 'ID': return 'ideal'; //'iDeal Internetbetaling'
case '21': return 'factuur'; //'Binnen 21 dagen na factuurdatum'
case '99': return 'banktransfer'; //'Overmaken op ING rekening o.v.v. factuurnummer'
case 'bo': return 'bol'; //'Voldaan via Bol.com'
case 'pa': return 'afterpay'; //'Afterpay'
case 'cr': return 'visa'; //'Creditcard betaling'
case 'PP': return 'paypal'; //'Paypal Internetbetaling'
case 'PI': return 'pin';
case 'MC': return 'mistercash'; //'Mister Cash Internetbetaling'
case '30': //'Netto binnen 30 dagen'
case '14': //'Binnen 14 dagen na factuurdatum'
case 'BA': //'Betaling per bank voor levering'
case '10': //'Is voldaan in winkel'
case '02': //'Vooraf via betaallink'
return 'banktransfer';
case 'CH':
case '12':
case 'KB':
case 'SP':
case 'be':
case 'af':
case 'vo':
case 'ma':
case '03':
return $method;
}
return $method;
}
private function update(SalesOrder $order, Sale $sale): void
{
try {
$attr = $order->attributes();
$info = $sale->getInfo();
$info['total_paid'] = (float)$attr['AmountFC'];
[$items, $notes] = $this->getItemsFromOrder($order);
$info['items'] = $items;
$info['description'] = $notes ? trim(implode("\n\n", $notes)) : '';
$sale->setInfo($info);
$this->em->persist($sale);
$this->em->flush();
} catch (\Exception $e) {
}
}
private function getItemsFromOrder(SalesOrder $order): array
{
$order_attr = $order->attributes();
$notes = [];
if ($description = trim($order_attr['Description'] ?? '')) {
$notes[] = $description;
}
$items = [];
foreach ($order->SalesOrderLines as $item) {
/* @var SalesOrderLine $item */
// Error? TODO @improve - do something with this?
if (is_array($item)) {
dump($item);
continue;
}
try {
$attr = $item->attributes();
} catch (\Exception $e) {
continue; // TODO @improve - do something with the error?
}
$items[] = [
'item_id' => $attr['ID'],
'product_id' => $attr['Item'],
'sku' => $attr['ItemCode'],
'name' => $attr['ItemDescription'],
'quantity' => (int)$attr['Quantity'],
'price' => (float)$attr['UnitPrice'],
'total' => (float)$attr['AmountFC'] + (float)$attr['VATAmount'],
'tax_invoiced' => (float)$attr['VATAmount'],
'tax_percent' => (float)$attr['VATPercentage'] * 100,
'options' => [
'volume' => $this->exact->getVolume($attr['Item'], (int)$attr['Quantity'])
],
];
if (isset($attr['Notes']) && $attr['Notes']) {
$notes[] = trim($attr['Notes']);
}
}
return [$items, $notes];
}
private function getDiscount(array $attr, $items)
{
if (!isset($attr['Discount']) || !$attr['Discount']) {
return 0;
}
$total = 0;
foreach ($items as $item) {
$total += $item['total'];
}
return round($total * $attr['Discount'], 2);
}
}