u
This commit is contained in:
@@ -104,7 +104,7 @@ const shipment = await store.order.createShipment(googleAuthId, 10);
|
|||||||
|
|
||||||
*Source: `src/store/order/delete.js`*
|
*Source: `src/store/order/delete.js`*
|
||||||
|
|
||||||
Deletes an order by its id within the user's selected company.
|
Deletes an order by its id within the user's selected company. Refuses to delete the order if any shipment manifest references it.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ Deletes an order by its id within the user's selected company.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If the order is not found.
|
`Error` — If the order is not found or has shipment manifests.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Creates a shipping (package) for a shipment manifest. The number is allocated fr
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If manifest is not found, carrier is not found, manifest is not a shipment, or input is invalid.
|
`Error` — If manifest is not found, carrier is not found, manifest is not a shipment, shipment is already shipped, or input is invalid.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,29 @@ OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_c
|
|||||||
WHERE id = @id;
|
WHERE id = @id;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHECK_SHIPMENT_ALREADY_SHIPPED = `
|
||||||
|
SELECT TOP 1 1
|
||||||
|
FROM manifest m
|
||||||
|
WHERE m.id = @manifestId
|
||||||
|
AND (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM shipping s
|
||||||
|
WHERE s.manifest_id = m.id AND LOWER(s.[state]) = 'shipped'
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM shipmentsummary ss
|
||||||
|
WHERE ss.manifest_id = m.id AND LOWER(ss.[state]) IN ('delivered', 'returned')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
export const CHECK_ORDER_SHIPPED = `
|
export const CHECK_ORDER_SHIPPED = `
|
||||||
SELECT TOP 1 s.id
|
SELECT TOP 1 s.id
|
||||||
FROM shipping s
|
FROM shipping s
|
||||||
WHERE s.manifest_id = @manifestId AND LOWER(s.[state]) = 'shipped';
|
WHERE s.manifest_id = @manifestId AND LOWER(s.[state]) = 'shipped';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const GET_ORDER_LINES_FOR_SHIPPING = `
|
export const GET_ORDER_LINES_FOR_SHIPPING = `
|
||||||
SELECT id, sku, name, amount
|
SELECT id, sku, name, amount
|
||||||
FROM manifestarticle
|
FROM manifestarticle
|
||||||
|
|||||||
@@ -51,7 +51,21 @@ FROM shipping
|
|||||||
WHERE manifest_id = @id AND LOWER([state]) = 'shipped';
|
WHERE manifest_id = @id AND LOWER([state]) = 'shipped';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHECK_ORDER_HAS_SHIPMENTS = `
|
||||||
|
SELECT TOP 1 m.id
|
||||||
|
FROM manifest m
|
||||||
|
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id AND ss.company_id = @companyId
|
||||||
|
WHERE m.company_id = @companyId
|
||||||
|
AND m.[type] = 'shipment'
|
||||||
|
AND (
|
||||||
|
m.[number] LIKE @pattern
|
||||||
|
OR ss.order_id = @orderId
|
||||||
|
OR ss.order_number = @orderNumber
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_SHIPMENT = `
|
export const DELETE_SHIPMENT = `
|
||||||
DELETE FROM manifest
|
DELETE FROM manifest
|
||||||
WHERE id = @id AND company_id = @companyId AND [type] = 'shipment';
|
WHERE id = @id AND company_id = @companyId AND [type] = 'shipment';
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const {
|
|||||||
SHIPPING_DELETE,
|
SHIPPING_DELETE,
|
||||||
GET_SHIPPING_BY_ID,
|
GET_SHIPPING_BY_ID,
|
||||||
SHIPPING_UPDATE,
|
SHIPPING_UPDATE,
|
||||||
|
CHECK_SHIPMENT_ALREADY_SHIPPED,
|
||||||
CHECK_ORDER_SHIPPED,
|
CHECK_ORDER_SHIPPED,
|
||||||
GET_ORDER_LINES_FOR_SHIPPING,
|
GET_ORDER_LINES_FOR_SHIPPING,
|
||||||
GET_MANIFEST_HEADER_BY_ID,
|
GET_MANIFEST_HEADER_BY_ID,
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ export const {
|
|||||||
GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN,
|
GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN,
|
||||||
GET_SHIPMENT_HEADER,
|
GET_SHIPMENT_HEADER,
|
||||||
CHECK_SHIPMENT_PACKAGES_SHIPPED,
|
CHECK_SHIPMENT_PACKAGES_SHIPPED,
|
||||||
|
CHECK_ORDER_HAS_SHIPMENTS,
|
||||||
DELETE_SHIPMENT,
|
DELETE_SHIPMENT,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
DELETE_MANIFEST as MSSQL_DELETE,
|
DELETE_MANIFEST as MSSQL_DELETE,
|
||||||
|
GET_MANIFEST_HEADER as MSSQL_GET_HEADER,
|
||||||
ORDER_TYPE as MSSQL_ORDER_TYPE,
|
ORDER_TYPE as MSSQL_ORDER_TYPE,
|
||||||
} from '../../providers/mssql/queries/manifestDoc.js';
|
} from '../../providers/mssql/queries/manifestDoc.js';
|
||||||
import {
|
import {
|
||||||
DELETE_MANIFEST as SQLITE_DELETE,
|
DELETE_MANIFEST as SQLITE_DELETE,
|
||||||
|
GET_MANIFEST_HEADER as SQLITE_GET_HEADER,
|
||||||
ORDER_TYPE as SQLITE_ORDER_TYPE,
|
ORDER_TYPE as SQLITE_ORDER_TYPE,
|
||||||
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
} from '../../providers/sqlite3/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
CHECK_ORDER_HAS_SHIPMENTS as MSSQL_CHECK_ORDER_HAS_SHIPMENTS,
|
||||||
|
GET_SHIPMENT_PREFIX as MSSQL_GET_SHIPMENT_PREFIX,
|
||||||
|
} from '../../providers/mssql/queries/shipment.js';
|
||||||
|
import {
|
||||||
|
CHECK_ORDER_HAS_SHIPMENTS as SQLITE_CHECK_ORDER_HAS_SHIPMENTS,
|
||||||
|
GET_SHIPMENT_PREFIX as SQLITE_GET_SHIPMENT_PREFIX,
|
||||||
|
} from '../../providers/sqlite3/queries/shipment.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -13,16 +23,24 @@ export default function del(provider) {
|
|||||||
const access = resolveSelectedCompanyAccess(provider);
|
const access = resolveSelectedCompanyAccess(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const getHeaderSql = isSqlite ? SQLITE_GET_HEADER : MSSQL_GET_HEADER;
|
||||||
|
const getShipmentPrefixSql = isSqlite
|
||||||
|
? SQLITE_GET_SHIPMENT_PREFIX
|
||||||
|
: MSSQL_GET_SHIPMENT_PREFIX;
|
||||||
|
const checkOrderHasShipmentsSql = isSqlite
|
||||||
|
? SQLITE_CHECK_ORDER_HAS_SHIPMENTS
|
||||||
|
: MSSQL_CHECK_ORDER_HAS_SHIPMENTS;
|
||||||
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
|
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an order by its id within the user's selected company.
|
* Deletes an order by its id within the user's selected company.
|
||||||
|
* Refuses to delete the order if any shipment manifest references it.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The id of the order to delete.
|
* @param {number} id - The id of the order to delete.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If the order is not found.
|
* @throws {Error} If the order is not found or has shipment manifests.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.order.delete(42, 5);
|
* await store.order.delete(42, 5);
|
||||||
@@ -32,18 +50,52 @@ export default function del(provider) {
|
|||||||
throw new Error('Order not found');
|
throw new Error('Order not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const runner = makeRunner(provider, tx);
|
const run = async (runner) => {
|
||||||
const { companyId } = await access(googleAuthId, runner);
|
const { companyId } = await access(googleAuthId, runner);
|
||||||
|
|
||||||
const result = await runner.query(deleteSql, {
|
const headerRes = await runner.query(getHeaderSql, {
|
||||||
id,
|
id,
|
||||||
companyId,
|
companyId,
|
||||||
type: orderType,
|
type: orderType,
|
||||||
});
|
});
|
||||||
|
const order = headerRes.rows[0];
|
||||||
|
if (!order) {
|
||||||
|
throw new Error('Order not found');
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.rowsAffected) {
|
const prefixRes = await runner.query(getShipmentPrefixSql, { companyId });
|
||||||
throw new Error('Order not found');
|
const shipmentPrefix = prefixRes.rows[0]?.prefix?.trim?.() || '-L-';
|
||||||
|
const pattern = `${order.number}${shipmentPrefix}%`;
|
||||||
|
|
||||||
|
const checkRes = await runner.query(checkOrderHasShipmentsSql, {
|
||||||
|
companyId,
|
||||||
|
orderId: id,
|
||||||
|
orderNumber: order.number,
|
||||||
|
pattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkRes.rows?.length > 0) {
|
||||||
|
throw new Error('Order has shipment manifests');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await runner.query(deleteSql, {
|
||||||
|
id,
|
||||||
|
companyId,
|
||||||
|
type: orderType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.rowsAffected) {
|
||||||
|
throw new Error('Order not found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return run(makeRunner(provider, tx));
|
||||||
}
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(run);
|
||||||
|
}
|
||||||
|
return run(makeRunner(provider));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function create(provider) {
|
|||||||
* @param {number} [input.warehouseId] - Optional warehouse ID for stock deduction (defaults to company's first warehouse).
|
* @param {number} [input.warehouseId] - Optional warehouse ID for stock deduction (defaults to company's first warehouse).
|
||||||
* @param {object} [tx] - Optional transaction to run within.
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>}
|
* @returns {Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>}
|
||||||
* @throws {Error} If manifest is not found, carrier is not found, manifest is not a shipment, or input is invalid.
|
* @throws {Error} If manifest is not found, carrier is not found, manifest is not a shipment, shipment is already shipped, or input is invalid.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
|
* await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
|
||||||
@@ -79,13 +79,12 @@ export default function create(provider) {
|
|||||||
throw new Error('Carrier not found');
|
throw new Error('Carrier not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
let alreadyShipped = false;
|
// Verify related shipment is not already shipped
|
||||||
if (isShipped) {
|
const shippedCheck = await runner.query(q.CHECK_SHIPMENT_ALREADY_SHIPPED, {
|
||||||
// Check if shipment was already marked shipped by an existing package
|
manifestId,
|
||||||
const shippedCheck = await runner.query(q.CHECK_ORDER_SHIPPED, {
|
});
|
||||||
manifestId,
|
if (shippedCheck.rows[0]) {
|
||||||
});
|
throw new Error('Shipment is already shipped');
|
||||||
alreadyShipped = Boolean(shippedCheck.rows[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate shipping number and insert shipping record
|
// Allocate shipping number and insert shipping record
|
||||||
@@ -108,7 +107,7 @@ export default function create(provider) {
|
|||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isShipped && !alreadyShipped) {
|
if (isShipped) {
|
||||||
// Load shipment manifest lines
|
// Load shipment manifest lines
|
||||||
const linesRes = await runner.query(q.GET_ORDER_LINES_FOR_SHIPPING, {
|
const linesRes = await runner.query(q.GET_ORDER_LINES_FOR_SHIPPING, {
|
||||||
manifestId,
|
manifestId,
|
||||||
|
|||||||
@@ -192,6 +192,64 @@ describe('store.shipment', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('shipping.create', () => {
|
||||||
|
it('allows adding shippings while shipment is unshipped, but refuses once shipped', async () => {
|
||||||
|
const order = await makeOrder();
|
||||||
|
const s = await store.shipment.create(gid, { orderId: order.id, lines: [{ sku: 'SH-A', amount: 5 }] });
|
||||||
|
const carrier = await store.carrier.create(gid, { name: `Carrier ${order.number}`, hasPackages: true });
|
||||||
|
|
||||||
|
// First package: draft -> allowed
|
||||||
|
const p1 = await store.shipping.create(gid, {
|
||||||
|
manifestId: s.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
});
|
||||||
|
assert.ok(p1.id);
|
||||||
|
|
||||||
|
// Second package while still draft -> allowed
|
||||||
|
const p2 = await store.shipping.create(gid, {
|
||||||
|
manifestId: s.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
});
|
||||||
|
assert.ok(p2.id);
|
||||||
|
|
||||||
|
// Now mark package as shipped -> shipment is now shipped
|
||||||
|
await store.shipping.update(gid, p1.id, { state: 'shipped' });
|
||||||
|
|
||||||
|
// Attempting to add a new shipping to the already shipped shipment must fail
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.shipping.create(gid, {
|
||||||
|
manifestId: s.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
}),
|
||||||
|
{ message: 'Shipment is already shipped' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses adding shipping if shipment is in delivered state', async () => {
|
||||||
|
const order = await makeOrder();
|
||||||
|
const s = await store.shipment.create(gid, { orderId: order.id, lines: [{ sku: 'SH-A', amount: 2 }] });
|
||||||
|
const carrier = await store.carrier.create(gid, { name: `Carrier Deliv ${order.number}`, hasPackages: true });
|
||||||
|
|
||||||
|
await store.shipment.update(gid, s.id, { state: 'delivered' });
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.shipping.create(gid, {
|
||||||
|
manifestId: s.id,
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'parcel',
|
||||||
|
state: 'draft',
|
||||||
|
}),
|
||||||
|
{ message: 'Shipment is already shipped' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('changes the state and validates it', async () => {
|
it('changes the state and validates it', async () => {
|
||||||
const order = await makeOrder();
|
const order = await makeOrder();
|
||||||
@@ -242,8 +300,24 @@ describe('store.shipment', () => {
|
|||||||
await assert.rejects(() => store.shipment.delete(gid, null), { message: 'Shipment is required' });
|
await assert.rejects(() => store.shipment.delete(gid, null), { message: 'Shipment is required' });
|
||||||
assert.ok(await store.shipment.getById(gid, s.id));
|
assert.ok(await store.shipment.getById(gid, s.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refuses to delete an order manifest when there is a shipment manifest referencing it', async () => {
|
||||||
|
const order = await makeOrder();
|
||||||
|
const s = await store.shipment.create(gid, { orderId: order.id, lines: [{ sku: 'SH-A', amount: 1 }] });
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.order.delete(gid, order.id),
|
||||||
|
{ message: 'Order has shipment manifests' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Once all shipment manifests referencing the order are deleted, the order can be deleted
|
||||||
|
await store.shipment.delete(gid, s.id);
|
||||||
|
await store.order.delete(gid, order.id);
|
||||||
|
await assert.rejects(() => store.order.getById(gid, order.id), { message: 'Order not found' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('list / rebuildSummaries', () => {
|
describe('list / rebuildSummaries', () => {
|
||||||
let lt;
|
let lt;
|
||||||
let o1;
|
let o1;
|
||||||
|
|||||||
Reference in New Issue
Block a user