Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/actions/submit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,16 @@ async function handleOrderStatus(ctx, formId, data) {

const body = transformSoapKeys(response);

// Detect cancellation from line item statuses. Cancelled orders may have no
// delivery items; each cancelled line item shows Status="Closed".
// Detect cancellation from line item statuses. Cancelled line items show
// Status="Closed" AND Quantity="0" — Status="Closed" alone can also mean
// shipped/fulfilled, so the zero quantity is what distinguishes cancellation.
const lineItems = [].concat(body.order?.lineItem ?? []);
if (lineItems.length > 0) {
const closedCount = lineItems.filter(item => item?.status === 'Closed').length;
if (closedCount === lineItems.length) {
const isCancelled = item => item?.status === 'Closed' && item?.quantity === '0';
const cancelledCount = lineItems.filter(isCancelled).length;
if (cancelledCount === lineItems.length) {
body.outcome = 'Cancelled';
} else if (closedCount > 0) {
} else if (cancelledCount > 0) {
body.outcome = 'Partially Cancelled';
}
}
Expand All @@ -236,10 +238,14 @@ async function handleOrderStatus(ctx, formId, data) {
delete body.order?.customer;
delete body.order?.lineItem;
delete body.order?.systemOfRecordKey;
body.order?.delivery?.forEach(delivery => {
delete delivery.systemOfRecordKey;
delete delivery.trackingDetail;
});
if (body.order?.delivery) {
// single-delivery responses arrive as an object, not an array — normalize
body.order.delivery = [].concat(body.order.delivery);
body.order.delivery.forEach(delivery => {
delete delivery.systemOfRecordKey;
delete delivery.trackingDetail;
});
}

return {
body,
Expand Down
87 changes: 84 additions & 3 deletions test/submit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ describe('submit action', () => {
expect(result.body.order).not.toHaveProperty('lineItem');
});

test('reports outcome=Partially Cancelled when some line items are Closed', async () => {
test('reports outcome=Partially Cancelled when some line items are Closed with Quantity=0', async () => {
const partialBody = {
Response: {
'@_Id': 'abc',
Expand All @@ -423,8 +423,8 @@ describe('submit action', () => {
'Order': {
'@_Key': 'om-partial',
'LineItem': [
{ '@_Key': '1', '@_Status': 'Closed' },
{ '@_Key': '2', '@_Status': 'Shipped' },
{ '@_Key': '1', '@_Status': 'Closed', '@_Quantity': '0' },
{ '@_Key': '2', '@_Status': 'Shipped', '@_Quantity': '1' },
],
},
},
Expand All @@ -438,13 +438,94 @@ describe('submit action', () => {
expect(result.body.outcome).toBe('Partially Cancelled');
});

test('does not flag Closed line items with Quantity>0 as cancelled (shipped/fulfilled)', async () => {
// Closed + Quantity=1 means delivered/fulfilled, not cancelled
const fulfilledBody = {
Response: {
'@_Id': 'abc',
'@_Outcome': 'Success',
'@_Succeeded': 'true',
'Order': {
'@_Key': 'om-fulfilled',
'LineItem': { '@_Key': '1', '@_Status': 'Closed', '@_Quantity': '1' },
},
},
};

mockMakeContext.mockResolvedValue(makeOrderCtx('om-fulfilled'));
mockQueryOrder.mockResolvedValue({ status: 200, body: fulfilledBody });

const result = await main({});
expect(result.body.outcome).toBe('Success');
});

test('preserves original outcome when no line items are Closed', async () => {
mockMakeContext.mockResolvedValue(makeOrderCtx());
mockQueryOrder.mockResolvedValue({ status: 200, body: successBody });

const result = await main({});
expect(result.body.outcome).toBe('Success');
});

test('handles a single Delivery (parsed as object, not array)', async () => {
// Real EBS response for a commercial order with one shipped Delivery.
// fast-xml-parser emits single-occurrence elements as objects, which
// previously crashed the handler at `delivery.forEach`.
const singleDeliveryBody = {
Response: {
'@_Id': '123456654321',
'@_Outcome': 'Success',
'@_Succeeded': 'true',
'Order': {
'@_Shipping': 'Standard',
'@_Source': 'US',
'@_Currency': 'USD',
'@_Type': 'Commercial',
'@_Key': 'om2101233469',
'@_SystemOfRecordKey': '14025466',
'@_Created': '2025-04-18T15:34:26',
'Customer': {
'@_Key': '11253571',
'@_SystemOfRecordKey': '7230937',
'First': 'DAVID',
'Last': 'NUESCHELER',
'Middle': '',
},
'Delivery': {
'@_Shipped': '2025-04-22T23:59:00',
'@_SystemOfRecordKey': '12208171',
'TrackingDetail': {
'@_Carrier': 'UPS',
'TrackingNumber': '1Z512Y240310631634',
'CustomCarrier': 'UPS',
'ShippingMethod': 'UPS Ground',
},
},
'LineItem': {
'@_Sku': '036019-ABAB',
'@_Quantity': '1',
'@_UnitSellingPrice': '1462.95',
'@_UnitOfMeasure': 'Each',
'@_Status': 'Closed',
'@_Key': '18115572',
},
},
},
};

mockMakeContext.mockResolvedValue(makeOrderCtx('om2101233469'));
mockQueryOrder.mockResolvedValue({ status: 200, body: singleDeliveryBody });

const result = await main({});
expect(result.statusCode).toBe(200);
expect(result.body.order.key).toBe('om2101233469');
expect(result.body.order.type).toBe('Commercial');
expect(Array.isArray(result.body.order.delivery)).toBe(true);
expect(result.body.order.delivery).toHaveLength(1);
expect(result.body.order.delivery[0].shipped).toBe('2025-04-22T23:59:00');
expect(result.body.order.delivery[0]).not.toHaveProperty('systemOfRecordKey');
expect(result.body.order.delivery[0]).not.toHaveProperty('trackingDetail');
});
});

// -- product-registration ------------------------------------------------
Expand Down
Loading