M3 Integration fixes pass 20

This commit is contained in:
Mark Gregory
2026-09-22 14:34:37 +01:00
parent 7765a57e22
commit 3c236469e5
5 changed files with 824 additions and 1032 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
<?php
namespace Modules\M3\Http\Controllers\API;
use Config;
use DB;
use File;
use Illuminate\Http\Request;
use Modules\M3\Http\Controllers\GetContentsController;
final class CloseShipmentPackageBuilder
{
public function __construct(private readonly array $context, private readonly mixed $freightTerms)
{
}
public function build(array $shipment, array $trailer): array
{
$state = ['packages' => [], 'errors' => '', 'coCharge' => [], 'details' => []];
foreach ($this->packageRows($shipment) as $row) {
$details = $this->packageDetails($shipment, $trailer, $row);
$state['errors'] .= $details['errors'];
$state['packages'][] = $details['package'];
$state['details'] = $details;
$state['coCharge'] = $this->coCharge($details);
}
return $state;
}
public function closeShipmentJson(array $shipment, array $trailer, array $state, int $endFlag): string
{
$details = $state['details'];
$payload = [
'DIVI' => $shipment['company'], 'WHS' => $shipment['whs'], 'DELIVERYNO' => $shipment['delivery'],
'PICKLISTSUFFIX' => $shipment['release'], 'TYPE' => 'CloseShipment', 'SCAC' => $details['scac'],
'CARRIER' => $details['carrier'], 'TRANSPORTMODE' => $details['transportMode'],
'SERVICETYPE' => $details['serviceType'], 'TRACKINGNUMBER' => $details['tracking'],
'PRONUMBER' => $trailer['pro'], 'BILLOFLADING' => $details['bol'], 'TRAILERNUMBER' => $trailer['trailer'],
'PACKAGES' => $state['packages'], 'DELIVERYNOTEDOC' => $this->documentUrl('PackingList', $shipment['delivery'], $shipment['delivery']),
'COMMODITIES' => $details['commodities']['value'], 'ENDFLAG' => $endFlag,
];
$serviceFields = $state['coCharge'] !== []
? ['shipType' => $details['shipType'], 'carrierServiceDescription' => $details['serviceName'], 'modeOfTransport' => $details['shipVia1']]
: ['ServiceType' => $details['shipType'], 'ServiceLevelCode' => $details['serviceName'], 'TransportationMode' => $details['shipVia1']];
return json_encode(array_merge($state['coCharge'], $payload, $serviceFields));
}
private function packageRows(array $shipment)
{
return DB::connection(config('m3.connection', 'pgsql'))->table('icm695m1')
->selectRaw('AYMACPKR,AYINTBOX,AYHEIGHT,AYWIDTH,AYDEPTH,AYTYPE,AYLNWGHT,AYID,AYSPR1A01,TRIM(AYSPR15A01) AS SCAC,TRIM(AYSPR30A01) AS TRACKING,AYTRUCK AS AGGREGATION,A4RATETCP AS FREIGHT,A4OFSTRM AS FREIGHTTERMS,A4RATEM3,A4CLOSEM3,A4SHIPTYPE,A4REFORD,A4ONO,A4SHIPVIA,A42SSVCNAM,A4SHIPVIA1')
->leftJoin('ICM688M1', function ($join) {
$join->on('AYCO', '=', 'A4CO')->on('AYWHS', '=', 'A4WHS')->on('AYMACPKR', '=', 'A4MACPKR');
})
->where('AYMACPKR', $shipment['delivery'])->where('AYCO', $shipment['company'])
->where('AYWHS', $shipment['whs'])->orderBy('AYINTBOX')->get();
}
private function packageDetails(array $shipment, array $trailer, object $row): array
{
$details = $this->rowDetails($row);
$details['carrier'] = $this->carrierName($details['scac']);
$details['bol'] = $details['scac'] === 'RETR' ? $trailer['pro'] : $details['bol'];
$details['errors'] = $this->shippingErrors($details, $trailer['pro']);
$details['errors'] .= $this->previousTransmissionError($shipment);
$details['bundle'] = $this->bundleType($details['bundle'], $row);
$details['commodities'] = $this->commodities($shipment['delivery'], $details['errors']);
$details['errors'] = $details['commodities']['errors'];
if ($details['billable'] === 'Y' && (float) $details['freight'] == 0 && $details['rateM3'] === '') {
$details['errors'] .= ' Please manually add a freight charge for this Prepaid and Add order; ';
}
$labels = $this->labels($shipment, $details['box'], $details['gs1Required']);
$details['errors'] .= $labels['error'];
$details['package'] = $this->packageArray($details, $labels);
return $details;
}
private function rowDetails(object $row): array
{
$freightTerms = trim($row->FREIGHTTERMS);
$currentFreightTerms = $this->freightTerms->where('FREIGHTTERMS', $freightTerms)->first();
return [
'shipType' => trim($row->A4SHIPTYPE), 'serviceName' => trim($row->A42SSVCNAM),
'shipVia1' => trim($row->A4SHIPVIA1), 'freightTerms' => $freightTerms,
'rateM3' => trim($row->A4RATEM3), 'freight' => trim($row->FREIGHT),
'refOrder' => trim($row->A4REFORD), 'orderNo' => trim($row->A4ONO),
'delivery' => trim($row->AYMACPKR), 'box' => trim($row->AYINTBOX),
'tracking' => trim($row->TRACKING), 'scac' => trim($row->SCAC),
'bundle' => trim($row->AYID) !== '' ? trim($row->AYID) : '',
'weight' => trim($row->AYLNWGHT), 'height' => trim($row->AYHEIGHT),
'width' => trim($row->AYWIDTH), 'depth' => trim($row->AYDEPTH),
'bol' => isset($row->AYSPR1A01) ? trim($row->AYSPR1A01) : '',
'transportMode' => trim(str_replace('&', '_', $row->A4SHIPVIA ?? '')),
'serviceType' => trim(str_replace('&', '_', $row->A4SHIPVIA ?? '')),
'billable' => isset($currentFreightTerms->BILLABLE) ? trim($currentFreightTerms->BILLABLE) : 'N',
'gs1Required' => $this->gs1Required($row), 'errors' => '',
];
}
private function shippingErrors(array $details, string $proNumber): string
{
$missingTracking = $details['scac'] === '' || $details['tracking'] === '';
$missingLtl = $details['shipType'] === 'LTL' && ($missingTracking || $details['bol'] === '');
$errors = ($missingTracking || $missingLtl)
? " Missing one or more of tracking number, BOL, or carrier. Please enter this data manually in 'invoicing' screen and retry this API call;"
: '';
if ($proNumber === '' && $details['scac'] === 'RETR') {
$errors .= ' Missing PARS info for this RETRans order. Please contact MPBA to ask them to enter the value for you, and then you can retry; ';
}
return $errors;
}
private function carrierName(string $scac): string
{
if ($scac === '') {
return '';
}
$row = DB::connection(config('m3.connection', 'pgsql'))->table('icm160m')
->select('CANAM')->where('CANUM', substr($scac, 0, 4))->where('CACO', $this->context['company'])->first();
return isset($row->CANAM) ? trim(str_replace('&', '_', $row->CANAM)) : '';
}
private function previousTransmissionError(array $shipment): string
{
$row = DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->select('SHMACPKR')->where('SHMACPKR', $shipment['delivery'])
->where('SHCO', $shipment['company'])->where('SHWHS', $shipment['whs'])
->whereIn('SHSOURCE', ['TransferAPIURL', 'CloseShipmentCoChargeAPI', 'PackSyst4tray'])
->where('SHERROR', 'X')->whereIn('SHTYPE', ['M3-Res-out', 'M3-QtyChg'])->first();
return isset($row->SHMACPKR)
? ' There are previous M3 transmissions in error for this delivery note that have not been addressed; '
: '';
}
private function bundleType(string $bundle, object $row): string
{
if ($bundle !== '') {
return $bundle;
}
$record = DB::connection(config('m3.connection', 'pgsql'))->table('icm693m')
->select('AEID')->where('AESPR1A01', 'D')->where('AETYPE', trim($row->AYTYPE))->first();
return isset($record->AEID) ? trim($record->AEID) : '';
}
private function commodities(string $delivery, string $errors): array
{
$request = new Request;
$request->setMethod('POST');
$request->request->add(['MACPKR' => $delivery]);
$responseData = (new GetContentsController)->processShipment($request)->getData();
$commodities = empty($responseData->error) ? ($responseData->commodities ?? '') : '';
return ['value' => $commodities, 'errors' => $commodities !== '' ? $errors : $errors . ' Could not find Commodities; '];
}
private function labels(array $shipment, string $box, bool $gs1Required): array
{
$carrier = $this->documentUrl('CarrierLabel', $shipment['delivery'], $box);
$content = $this->documentUrl('ContentLabel', $shipment['delivery'], $box);
$gs1 = '';
if ($gs1Required) {
$row = DB::connection(config('m3.connection', 'pgsql'))->table('icm695m3')
->select('A2GS1LABEL')->where('A2CO', $shipment['company'])->where('A2WHS', $shipment['whs'])
->where('A2INTBOX', $box)->first();
$gs1 = isset($row->A2GS1LABEL) ? trim($row->A2GS1LABEL) : '';
}
return [
'carrier' => $carrier, 'content' => $content, 'gs1' => $gs1,
'error' => $gs1Required && $gs1 === ''
? ' Missing G1-128 Label. Please contact MPBA to ask them to enter the value for you, and then you can retry; '
: '',
];
}
private function documentUrl(string $type, string $delivery, string $documentNumber): string
{
$key = $type . '/' . $type . '_' . $delivery . '_' . $documentNumber . '_' . $this->context['company'] . '_' . $this->context['whs'];
if (! File::exists(Config::get('arrays.PdfPath') . $key . '.pdf')) {
return '';
}
return route('m3.M3GetPDFFromCache.index', ['MACPKR' => $delivery, 'DOCTYPE' => $type]);
}
private function gs1Required(object $row): bool
{
$record = DB::connection(config('m3.connection', 'pgsql'))->table('icm666m')
->select('A1SPR1A01')->where('A1CO', $this->context['company'])->where('A1WHS', $this->context['whs'])
->where('A1DELNO', trim($row->AYMACPKR))->first();
return (int) ($record->A1SPR1A01 ?? 0) === 1;
}
private function packageArray(array $details, array $labels): array
{
return [
'TRACKINGID' => $details['tracking'], 'BUNDLES' => $details['bundle'],
'WEIGHT' => $details['weight'], 'HEIGHT' => $details['height'], 'WIDTH' => $details['width'],
'LENGTH' => $details['depth'], 'SSCC' => $labels['gs1'],
'SHIPLABELDOC' => $labels['carrier'], 'CONTENTLABELDOC' => $labels['content'],
];
}
private function coCharge(array $details): array
{
if ($details['billable'] !== 'Y' || (float) $details['freight'] <= 0 || $details['rateM3'] !== '') {
return [];
}
$discountKey = $details['shipType'] === 'LTL'
? 'arrays.misc.discountCarrierRateForCustomerPriceLTL'
: 'arrays.misc.discountCarrierRateForCustomerPriceParcel';
$shippingToBill = (float) $details['freight'] * (100.00 - Config::get($discountKey)) / 100;
return ['QUANTITY' => round($shippingToBill, 2), 'ORDERCATEGORY' => $details['refOrder'], 'CONUMBER' => $details['orderNo']];
}
}
@@ -0,0 +1,258 @@
<?php
namespace Modules\M3\Http\Controllers\API;
use Config;
use DB;
use File;
use Http;
use Illuminate\Http\Request;
use Log;
use Modules\M3\Helpers\Helper;
use Modules\M3\Http\Controllers\GetContentsController;
use Str;
use Throwable;
final class CloseShipmentProcessor
{
private const ZERO_PAD = '000000';
private const JSON_CONTENT_TYPE = 'application/json';
private const NULL_M3_RESPONSE = 'M3 API responded with null value';
private const SOURCE = 'CloseShipmentCoChargeAPI';
private array $context;
public function __construct(private readonly Request $request)
{
$this->context = $this->makeContext();
}
public function handle()
{
try {
return $this->process();
} catch (Throwable $throwable) {
return $this->exceptionResponse($throwable);
}
}
private function process()
{
if ($this->context['params'] === []) {
$this->log('M3-Res-out', 'No URL Parameters Sent', 'X');
return response()->json([
'code' => '405',
'status' => 'Error',
'message' => 'No URL Parameters Sent',
]);
}
$this->logInitialRequest();
$tokenResult = $this->receiveToken();
if ($tokenResult['response'] !== null) {
return $tokenResult['response'];
}
$shipments = $this->loadShipments();
$shipments = $this->removeAlreadyClosed($shipments);
$freightTerms = DB::connection(config('m3.connection', 'pgsql'))->table('icm321m')
->selectRaw('TRIM(ARCODE) as FREIGHTTERMS,ARCHARGE as BILLABLE')
->get();
return $this->dispatchShipments($shipments, $freightTerms, $tokenResult['token']);
}
private function makeContext(): array
{
$params = $this->request->all();
$manifest = (string) ($params['MANIFEST'] ?? '');
$transType = (string) ($params['TRANSTYPE'] ?? 'M');
return [
'params' => $params,
'whs' => (string) ($params['WHS'] ?? ''),
'company' => (string) ($params['COMPANY'] ?? ''),
'manifest' => $manifest,
'transType' => $transType !== '' ? $transType : 'M',
'shdt' => (string) ($params['SHDT'] ?? ''),
'shtime' => (string) ($params['SHTIME'] ?? ''),
'shtype' => (string) ($params['SHTYPE'] ?? ''),
'user' => m3_user() !== '' ? Str::upper(m3_user()) : 'WebUser',
'delivery' => $manifest !== '' ? $manifest : self::ZERO_PAD,
'jsonIn' => '',
];
}
private function logInitialRequest(): void
{
$this->context['jsonIn'] = json_encode([
'WHS' => $this->context['whs'],
'COMPANY' => $this->context['company'],
'MANIFEST-MACPKR' => $this->context['manifest'],
'TRANSTYPE' => $this->context['transType'],
]);
$this->log('M3-Req-out', $this->context['jsonIn']);
}
private function receiveToken(): array
{
$response = Helper::checkReceiveTokenV2();
$data = $response->getData();
if ((string) ($data->code ?? '') === '200') {
return ['token' => (string) ($data->token ?? ''), 'response' => null];
}
$this->log('M3-Res-out', (string) ($data->message ?? ''), 'X');
return ['token' => '', 'response' => $response];
}
private function loadShipments()
{
$type = $this->context['transType'];
if (! in_array($type, ['M', 'P', 'Z'], true)) {
return collect();
}
if ($type === 'P') {
$this->acknowledgeRetry();
}
$query = $type === 'Z' ? $this->zeroPickQuery() : $this->standardShipmentQuery($type);
return $query->get();
}
private function standardShipmentQuery(string $type)
{
$query = DB::connection(config('m3.connection', 'pgsql'))->table('icm695m1')
->select('AYCO', 'AYWHS', 'AYMACPKR', 'AYRLNO', 'AYAPRF1551')
->distinct()
->where('AYCO', $this->context['company'])
->where('AYWHS', $this->context['whs'])
->orderBy('AYMACPKR');
$field = $type === 'P' ? 'AYMACPKR' : 'AYAPRF1551';
return $query->where($field, $this->context['manifest']);
}
private function zeroPickQuery()
{
return DB::connection(config('m3.connection', 'pgsql'))->table('icm688m1')
->select()->distinct()
->where('A4CO', $this->context['company'])
->where('A4WHS', $this->context['whs'])
->where('A4MACPKR', $this->context['manifest'])
->orderBy('A4MACPKR');
}
private function acknowledgeRetry(): void
{
if ($this->context['shdt'] !== '' && $this->context['shtime'] !== '' && $this->context['shtype'] !== '') {
DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->where('SHMACPKR', $this->context['manifest'])
->where('SHDT', $this->context['shdt'])->where('SHTIME', $this->context['shtime'])
->where('SHCO', $this->context['company'])->where('SHWHS', $this->context['whs'])
->where('SHSOURCE', self::SOURCE)->where('SHERROR', 'X')
->where('SHTYPE', $this->context['shtype'])
->update($this->acknowledgementValues());
}
$this->context['lineAck'] = $this->acknowledgedLines();
}
private function acknowledgedLines(): array
{
$rows = DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->where('SHMACPKR', $this->context['manifest'])
->where('SHCO', $this->context['company'])->where('SHWHS', $this->context['whs'])
->where('SHSOURCE', 'PackSyst4tray')->where('SHERROR', 'Z')->where('SHTYPE', 'M3-QtyChg')
->get();
return $rows->map(fn ($row) => Helper::getSubstringBetween($row->SHDATA, 'PickerLine:', ' -'))->all();
}
private function removeAlreadyClosed($shipments)
{
$field = $this->context['transType'] === 'Z' ? 'A4MACPKR' : 'AYMACPKR';
$deliveryNumbers = $shipments->map(fn ($row) => trim($row->{$field}))->all();
if ($deliveryNumbers === []) {
return $shipments;
}
$closed = DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->whereIn('SHMACPKR', $deliveryNumbers)
->where('SHCO', $this->context['company'])->where('SHWHS', $this->context['whs'])
->where('SHSOURCE', self::SOURCE)->where('SHERROR', '')
->where('SHTYPE', 'M3-Res-out')->where('SHDATA', 'like', '%Success%')
->pluck('SHMACPKR')->all();
return $shipments->reject(fn ($row) => in_array($row->{$field}, $closed));
}
private function dispatchShipments($shipments, $freightTerms, string $token)
{
if ($shipments->isEmpty()) {
return $this->noRecordsResponse();
}
if ($this->context['transType'] === 'Z') {
return (new CloseShipmentZeroPickProcessor($this->context, $token))->process($shipments);
}
return (new CloseShipmentStandardProcessor($this->context, $token, $freightTerms))->process($shipments);
}
private function noRecordsResponse()
{
$delivery = $this->context['manifest'] !== '' ? $this->context['manifest'] : self::ZERO_PAD;
$this->context['delivery'] = $delivery;
$message = 'No records found in ICM695M1,or URL parameters incomplete';
$this->log('M3-Req-out', $message, 'X');
return response()->json([
'code' => '405',
'status' => 'Error',
'message' => $message . ' For MacPkr/Manifest:' . $delivery,
]);
}
private function exceptionResponse(Throwable $throwable)
{
$isCurlError = str_contains($throwable->getMessage(), 'cURL error');
$message = $isCurlError
? 'Curl Error: Communication with M3 Timed Out'
: 'An Error Occured - ' . substr((string) $throwable, 0, 300);
$this->log('M3-Res-out', $message . ($isCurlError ? ' M3-JSN-out' . $this->context['jsonIn'] : ''), 'X');
if (! $isCurlError) {
Log::error($throwable);
}
return response()->json([
'code' => '405',
'status' => 'error',
'message' => $isCurlError ? $message . '.' : 'Catched Error: ' . $throwable,
]);
}
private function log(string $type, string $message, string $error = ''): void
{
Helper::logToICM699M([
'SHMACPKR' => $this->context['delivery'],
'SHDT' => date('Y-m-d'), 'SHTIME' => date('H:i:s'),
'SHERROR' => $error, 'SHINTBOX' => '', 'SHTYPE' => $type, 'SHDATA' => $message,
'SHUSER' => $this->context['user'], 'SHSOURCE' => self::SOURCE,
'SHCO' => $this->context['company'], 'SHWHS' => $this->context['whs'],
]);
}
private function acknowledgementValues(): array
{
return [
'SHERROR' => 'Z', 'SHACKUSER' => $this->context['user'],
'SHACKDATE' => date('Y-m-d'), 'SHACKTIME' => date('H:i:s'),
];
}
}
@@ -0,0 +1,207 @@
<?php
namespace Modules\M3\Http\Controllers\API;
use Config;
use DB;
use Http;
use Log;
use Modules\M3\Helpers\Helper;
final class CloseShipmentStandardProcessor
{
private const SOURCE = 'CloseShipmentCoChargeAPI';
private const JSON_CONTENT_TYPE = 'application/json';
private const NULL_M3_RESPONSE = 'M3 API responded with null value';
public function __construct(
private array $context,
private readonly string $token,
private readonly mixed $freightTerms,
) {
}
public function process($shipments)
{
$allErrors = '';
$lastShipment = null;
foreach ($shipments as $shipment) {
$lastShipment = $this->shipmentContext($shipment);
$allErrors .= $this->processShipment($lastShipment);
}
if ($lastShipment !== null) {
$this->closeFalsePositiveErrors($lastShipment);
}
$this->markManifestProcessed();
return $this->resultResponse($allErrors);
}
private function shipmentContext(object $shipment): array
{
return [
'whs' => trim($shipment->AYWHS),
'company' => trim($shipment->AYCO),
'delivery' => trim($shipment->AYMACPKR),
'release' => trim($shipment->AYRLNO),
'manifest' => trim($shipment->AYAPRF1551),
];
}
private function processShipment(array $shipment): string
{
$endFlag = $this->shortPickEndFlag($shipment);
$trailer = $this->trailerContext($shipment);
$builder = new CloseShipmentPackageBuilder($this->context, $this->freightTerms);
$state = $builder->build($shipment, $trailer);
if ($state['packages'] === []) {
return $state['errors'];
}
$json = $builder->closeShipmentJson($shipment, $trailer, $state, $endFlag);
if ($state['errors'] !== '') {
$this->logValidationErrors($shipment, $state['errors']);
return $state['errors'];
}
return $this->sendCloseRequest($shipment, $json, $state['coCharge']);
}
private function shortPickEndFlag(array $shipment): int
{
$rows = DB::connection(config('m3.connection', 'pgsql'))->table('icm688m2')
->select('A5PART', 'A5PACKED', 'A5PICKED', 'A5PKLN', 'A5RSVQTY')->distinct()
->where('A5CO', $shipment['company'])->where('A5WHS', $shipment['whs'])
->where('A5MACPKR', $shipment['delivery'])->where('A5RLNO', $shipment['release'])
->orderBy('A5PART')->get();
foreach ($rows as $row) {
if ((int) ($row->A5RSVQTY ?? 0) - (int) ($row->A5PICKED ?? 0) > 0) {
return 1;
}
}
return 0;
}
private function trailerContext(array $shipment): array
{
$row = DB::connection(config('m3.connection', 'pgsql'))->table('icm705m1')
->select('APTRUCKID', 'APTRACKNO', 'APMANIFEST')
->where('APCO', $this->context['company'])->where('APMANIFEST', $shipment['manifest'])
->where('APWHS', $this->context['whs'])->first();
return [
'trailer' => isset($row->APTRUCKID) ? trim($row->APTRUCKID) : '',
'pro' => isset($row->APTRACKNO) ? trim($row->APTRACKNO) : '',
];
}
private function sendCloseRequest(array $shipment, string $json, array $coCharge): string
{
Log::channel('dailyCron')->info('1 >>> JSON Sent for Manifest: ' . (string) (int) $shipment['manifest'] . ' ,Macpicker: ' . $shipment['delivery']);
$this->log($shipment, 'M3-JSN-out', $json);
$response = Http::withHeaders(['accept' => self::JSON_CONTENT_TYPE, 'Content-Type' => self::JSON_CONTENT_TYPE, 'Connection' => 'close'])
->withToken($this->token)->withBody($json, self::JSON_CONTENT_TYPE)
->timeout(Config::get('arrays.curlTimeOut'))->retry(3, 100)
->post(env('ENDPOINTURL') . 'APIFLOWS/Sentinel/TransferAPI?ionapiRespStyle=sync');
Log::channel('dailyCron')->info('2 >>> Close API M3 Responded...');
$result = $this->parseM3Response($response, $shipment, $coCharge !== []);
if ($result['success']) {
$this->log($shipment, 'M3-Res-out', 'Success...' . json_encode($result['responseResult']) . $result['session']);
return '';
}
$this->markShipmentFailed($shipment);
$this->log($shipment, 'M3-Res-out', 'Important-Error: ' . $result['errors'] . json_encode($result['message']) . $result['session'], 'X');
return $result['errors'] . ' ';
}
private function parseM3Response($response, array $shipment, bool $hasCoCharge): array
{
$status = $response->getStatusCode();
$session = $response->hasHeader('x-transaction-id') ? '-sessionID:' . $response->header('x-transaction-id') : '-sessionID';
$contents = json_decode(Helper::sanityzeString($response->getBody()->getContents()));
if ($contents === null || (string) $status !== '200') {
return ['success' => false, 'message' => self::NULL_M3_RESPONSE, 'responseResult' => '', 'errors' => $status, 'session' => $session];
}
$message = $contents->result->CloseShipment;
$apiResult = $message->PickingListClose->Result;
$responseResult = $contents->result;
$errors = Helper::checkM3Errors($responseResult);
if ($apiResult !== 'Error') {
$this->markShipmentSuccessful($shipment, $hasCoCharge);
}
return ['success' => $apiResult !== 'Error', 'message' => $message, 'responseResult' => $responseResult, 'errors' => $errors, 'session' => $session];
}
private function markShipmentSuccessful(array $shipment, bool $hasCoCharge): void
{
$values = $hasCoCharge ? ['A4RATEM3' => 'X', 'A4PICKSTS' => 'A'] : ['A4PICKSTS' => 'A'];
DB::connection(config('m3.connection', 'pgsql'))->table('icm688m1')
->where('A4MACPKR', $shipment['delivery'])->where('A4CO', $shipment['company'])
->where('A4WHS', $shipment['whs'])->update($values);
}
private function markShipmentFailed(array $shipment): void
{
DB::connection(config('m3.connection', 'pgsql'))->table('icm688m1')
->where('A4MACPKR', $shipment['delivery'])->where('A4CO', $shipment['company'])
->where('A4WHS', $shipment['whs'])->update(['A4PICKSTS' => 'a']);
}
private function logValidationErrors(array $shipment, string $errors): void
{
$this->log($shipment, 'M3-Res-out', Helper::removeDuplicateSubstrings($errors), 'X');
}
private function closeFalsePositiveErrors(array $shipment): void
{
DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->where('SHMACPKR', $shipment['delivery'])->where('SHCO', $this->context['company'])
->where('SHWHS', $this->context['whs'])->where('SHSOURCE', self::SOURCE)->where('SHERROR', 'X')
->where('SHTYPE', 'M3-General')->where('SHDATA', 'like', '%CommTCP_read%')
->update(['SHERROR' => 'Z', 'SHACKUSER' => $this->context['user'], 'SHACKDATE' => date('Y-m-d'),
'SHACKTIME' => date('H:i:s'), 'SHACKNOTE' => 'Sentinel automatically closed the error (false positive CommTCP_read)']);
}
private function markManifestProcessed(): void
{
if ($this->context['transType'] !== 'M') {
return;
}
DB::connection(config('m3.connection', 'pgsql'))->table('icm705m1')
->where('APMANIFEST', $this->context['manifest'])->where('APCO', $this->context['company'])
->where('APWHS', $this->context['whs'])->update(['APSPARE2' => 'X']);
}
private function resultResponse(string $errors)
{
if ($errors === '') {
return response()->json(['code' => '200', 'status' => 'Success', 'message' => 'All Operations Successful']);
}
return response()->json([
'code' => '206', 'status' => 'Success With Warnings',
'message' => Helper::removeDuplicateSubstrings($errors) . '; Check Transaction Logs for Errors or Warnings.',
]);
}
private function log(array $shipment, string $type, string $message, string $error = ''): void
{
Helper::logToICM699M([
'SHMACPKR' => $shipment['delivery'], 'SHDT' => date('Y-m-d'), 'SHTIME' => date('H:i:s'),
'SHERROR' => $error, 'SHINTBOX' => '', 'SHTYPE' => $type, 'SHDATA' => $message,
'SHUSER' => $this->context['user'], 'SHSOURCE' => self::SOURCE,
'SHCO' => $this->context['company'], 'SHWHS' => $this->context['whs'],
]);
}
}
@@ -0,0 +1,133 @@
<?php
namespace Modules\M3\Http\Controllers\API;
use Config;
use DB;
use Http;
use Modules\M3\Helpers\Helper;
final class CloseShipmentZeroPickProcessor
{
private const SOURCE = 'CloseShipmentCoChargeAPI';
private const JSON_CONTENT_TYPE = 'application/json';
private const NULL_M3_RESPONSE = 'M3 API responded with null value';
public function __construct(private readonly array $context, private readonly string $token)
{
}
public function process($shipments)
{
$errors = '';
foreach ($shipments as $row) {
$shipment = $this->shipmentContext($row);
$errors .= $this->closeShipment($shipment);
$this->closeFalsePositiveErrors($shipment);
}
return $this->resultResponse($errors);
}
private function shipmentContext(object $row): array
{
return [
'delivery' => trim($row->A4MACPKR),
'whs' => trim($row->A4WHS),
'company' => trim($row->A4CO),
];
}
private function closeShipment(array $shipment): string
{
$json = json_encode([
'DIVI' => $shipment['company'], 'WHS' => $shipment['whs'],
'DELIVERYNO' => $shipment['delivery'], 'TYPE' => 'CloseShipment',
'PICKLISTSUFFIX' => '1', 'ENDFLAG' => '1',
]);
$this->log($shipment, 'M3-JSN-out', $json);
$response = Http::withHeaders([
'accept' => self::JSON_CONTENT_TYPE, 'Content-Type' => self::JSON_CONTENT_TYPE, 'Connection' => 'close',
])->withToken($this->token)->withBody($json, self::JSON_CONTENT_TYPE)
->timeout(Config::get('arrays.curlTimeOut'))->retry(2, 100)
->post(env('ENDPOINTURL') . 'APIFLOWS/Sentinel/TransferAPI?ionapiRespStyle=sync');
$result = $this->parseResponse($response, $shipment);
if ($result['success']) {
$this->log($shipment, 'M3-Res-out', 'Success' . $result['session']);
return '';
}
$this->markFailed($shipment);
$this->log($shipment, 'M3-Res-out', 'Important-Error: ' . $result['errors'] . json_encode($result['message']) . $result['session'], 'X');
return $result['errors'] . '-';
}
private function parseResponse($response, array $shipment): array
{
$contents = json_decode($response->getBody()->getContents());
$session = $response->hasHeader('x-transaction-id') ? '-sessionID:' . $response->header('x-transaction-id') : '-sessionID';
if ($contents === null) {
return ['success' => false, 'message' => self::NULL_M3_RESPONSE, 'errors' => '', 'session' => $session];
}
$message = $contents->result->CloseShipment;
$apiResult = $message->PickingListClose->Result;
$errors = Helper::checkM3Errors($contents->result);
if ($apiResult !== 'Error') {
$this->markSuccessful($shipment);
}
return ['success' => $apiResult !== 'Error', 'message' => $message, 'errors' => $errors, 'session' => $session];
}
private function markSuccessful(array $shipment): void
{
DB::connection(config('m3.connection', 'pgsql'))->table('icm688m1')
->where('A4MACPKR', $shipment['delivery'])->where('A4CO', $shipment['company'])
->where('A4WHS', $shipment['whs'])->where('A4PICKSTS', '!=', 'X')->update(['A4PICKSTS' => 'A']);
}
private function markFailed(array $shipment): void
{
DB::connection(config('m3.connection', 'pgsql'))->table('icm688m1')
->where('A4MACPKR', $shipment['delivery'])->where('A4CO', $shipment['company'])
->where('A4WHS', $shipment['whs'])->where('A4PICKSTS', '!=', 'X')->update(['A4PICKSTS' => 'a']);
}
private function closeFalsePositiveErrors(array $shipment): void
{
DB::connection(config('m3.connection', 'pgsql'))->table('icm699m')
->where('SHMACPKR', $shipment['delivery'])->where('SHCO', $shipment['company'])
->where('SHWHS', $shipment['whs'])->where('SHSOURCE', self::SOURCE)->where('SHERROR', 'X')
->where('SHTYPE', 'M3-General')->where('SHDATA', 'like', '%CommTCP_read%')
->update([
'SHERROR' => 'Z', 'SHACKUSER' => $this->context['user'], 'SHACKDATE' => date('Y-m-d'),
'SHACKTIME' => date('H:i:s'), 'SHACKNOTE' => 'Sentinel automatically closed the error (false positive CommTCP_read)',
]);
}
private function resultResponse(string $errors)
{
if ($errors === '') {
return response()->json(['code' => '200', 'status' => 'Success', 'message' => 'All Operations Successful']);
}
return response()->json([
'code' => '206', 'status' => 'Success With Warnings',
'message' => $errors . '; Check The Logs for Errors or Warnings.',
]);
}
private function log(array $shipment, string $type, string $message, string $error = ''): void
{
Helper::logToICM699M([
'SHMACPKR' => $shipment['delivery'], 'SHDT' => date('Y-m-d'), 'SHTIME' => date('H:i:s'),
'SHERROR' => $error, 'SHINTBOX' => '', 'SHTYPE' => $type, 'SHDATA' => $message,
'SHUSER' => $this->context['user'], 'SHSOURCE' => self::SOURCE,
'SHCO' => $shipment['company'], 'SHWHS' => $shipment['whs'],
]);
}
}