This commit is contained in:
seb
2026-10-17 05:10:28 +02:00
parent a194cf6b8b
commit f8cc01baf0
25 changed files with 1034 additions and 54 deletions

176
API.carrier.md Normal file
View File

@@ -0,0 +1,176 @@
# carrier
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `carrier.create`
*Source: `src/store/carrier/create.js`*
Creates a new carrier for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Carrier input. |
| `input.name` | `string` | The carrier name. |
| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). |
| `[input.hasPackages=false]` | `boolean` | Whether this carrier uses packages. |
| `[input.isReverse=false]` | `boolean` | Whether this carrier handles reverse shipments. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The created carrier.
**Throws**
`Error` — If name is missing, sort is invalid, or carrier already exists.
**Example**
```js
const c = await store.carrier.create(googleAuthId, { name: 'DHL', hasPackages: true });
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
```
## `carrier.delete`
*Source: `src/store/carrier/delete.js`*
Deletes a carrier for the user's selected company, unless referenced in shipping.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID to delete. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — If carrier not found or in use in shipping.
**Example**
```js
await store.carrier.delete(googleAuthId, 1);
```
## `carrier.getById`
*Source: `src/store/carrier/getById.js`*
Retrieves a carrier by its ID for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>` — The carrier or undefined.
**Example**
```js
const c = await store.carrier.getById(googleAuthId, 1);
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
```
## `carrier.list`
*Source: `src/store/carrier/list.js`*
Lists all carriers for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>` — List of carriers.
**Example**
```js
const carriers = await store.carrier.list(googleAuthId);
// => [{ id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }]
```
## `carrier.order`
*Source: `src/store/carrier/order.js`*
Reorders the carriers of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | The reorder input. |
| `input.carrierIds` | `Array<number>` | The carrier ids in their new order. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{carrierIds: Array<number>}>` — The applied order.
**Throws**
`Error` — If the order is invalid.
**Example**
```js
await store.carrier.order(googleAuthId, { carrierIds: [2, 1, 3] });
```
## `carrier.update`
*Source: `src/store/carrier/update.js`*
Updates a carrier name, sort order, hasPackages, and/or isReverse for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID to update. |
| `input` | `object` | Update payload. |
| `[input.name]` | `string` | The new carrier name. |
| `[input.sort]` | `number` | The new sort order. |
| `[input.hasPackages]` | `boolean` | Whether carrier uses packages. |
| `[input.isReverse]` | `boolean` | Whether carrier handles reverse shipments. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The updated carrier.
**Throws**
`Error` — If carrier not found, name is empty, sort is invalid, or carrier already exists.
**Example**
```js
const updated = await store.carrier.update(googleAuthId, 1, { name: 'DHL Express', hasPackages: true });
// => { id: 1, name: 'DHL Express', sort: 0, hasPackages: true, isReverse: false }
```

9
API.md
View File

@@ -224,7 +224,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `syncSummary(): void` - `syncSummary(): void`
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void` - `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
- [shipping](API.shipping.md) - [shipping](API.shipping.md)
- `create(googleAuthId: number, input: {manifestId: number, type: string, state: string, trackingCode?: string, carrier?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>` - `create(googleAuthId: number, input: {manifestId: number, carrierId: number, type: string, state: string, trackingCode?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>` - `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void` - `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
@@ -335,6 +335,13 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- [articlelist](API.articlelist.md) - [articlelist](API.articlelist.md)
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>` - `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>` - `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
- [carrier](API.carrier.md)
- `create(googleAuthId: number, input: {name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>`
- `order(googleAuthId: number, input: {carrierIds: Array<number>}, tx?: object): Promise<{carrierIds: Array<number>}>`
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
- [categoryattributevalue](API.categoryattributevalue.md) - [categoryattributevalue](API.categoryattributevalue.md)
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>` - `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
- `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>` - `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`

View File

@@ -15,25 +15,25 @@ Creates a shipping (package) for a shipment manifest. The number is allocated fr
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Shipping input. | | `input` | `object` | Shipping input. |
| `input.manifestId` | `number` | The shipment manifest id. | | `input.manifestId` | `number` | The shipment manifest id. |
| `input.carrierId` | `number` | The carrier id. |
| `input.type` | `string` | Shipping type. | | `input.type` | `string` | Shipping type. |
| `input.state` | `string` | Shipping state ('shipped' or other). | | `input.state` | `string` | Shipping state ('shipped' or other). |
| `[input.trackingCode]` | `string` | Carrier tracking code. | | `[input.trackingCode]` | `string` | Carrier tracking code. |
| `[input.carrier]` | `string` | Carrier name. |
| `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). | | `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>` `Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
**Throws** **Throws**
`Error` — If manifest 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, or input is invalid.
**Example** **Example**
```js ```js
await store.shipping.create(googleAuthId, { manifestId: 42, type: 'parcel', state: 'shipped', trackingCode: '1Z999', carrier: 'UPS' }); await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
``` ```
## `shipping.delete` ## `shipping.delete`

142
scripts/test-carrier.js Normal file
View File

@@ -0,0 +1,142 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Carrier Store API ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-carrier-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('carrier-test@example.com');
const company = await store.company.create({
name: 'Carrier Test Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create carrier with defaults
const c1 = await store.carrier.create(googleAuthId, { name: 'DHL' });
assert.equal(c1.name, 'DHL');
assert.equal(c1.sort, 0);
assert.equal(c1.hasPackages, false);
assert.equal(c1.isReverse, false);
console.log('PASS: Carrier 1 created with defaults');
// 2. Create carrier with full flags
const c2 = await store.carrier.create(googleAuthId, {
name: 'UPS',
sort: 5,
hasPackages: true,
isReverse: true,
});
assert.equal(c2.name, 'UPS');
assert.equal(c2.sort, 5);
assert.equal(c2.hasPackages, true);
assert.equal(c2.isReverse, true);
console.log('PASS: Carrier 2 created with custom flags');
// 3. Auto-increment sort
const c3 = await store.carrier.create(googleAuthId, {
name: 'DPD',
hasPackages: true,
});
assert.equal(c3.name, 'DPD');
assert.equal(c3.sort, 6);
assert.equal(c3.hasPackages, true);
assert.equal(c3.isReverse, false);
console.log('PASS: Carrier 3 auto-incremented sort');
// 4. Duplicate name validation
await assert.rejects(
() => store.carrier.create(googleAuthId, { name: 'DHL' }),
{ message: 'Carrier already exists' },
);
console.log('PASS: Duplicate carrier rejected');
// 5. Empty name validation
await assert.rejects(
() => store.carrier.create(googleAuthId, { name: ' ' }),
{ message: 'Name required' },
);
console.log('PASS: Empty carrier name rejected');
// 6. getById
const fetched = await store.carrier.getById(googleAuthId, c2.id);
assert.deepEqual(fetched, c2);
const notFound = await store.carrier.getById(googleAuthId, 9999);
assert.equal(notFound, undefined);
console.log('PASS: getById fetched carrier correctly');
// 7. list
const list = await store.carrier.list(googleAuthId);
assert.equal(list.length, 3);
assert.equal(list[0].name, 'DHL');
assert.equal(list[1].name, 'UPS');
assert.equal(list[2].name, 'DPD');
console.log('PASS: list returned sorted carriers');
// 8. update
const updated = await store.carrier.update(googleAuthId, c1.id, {
name: 'DHL Express',
hasPackages: true,
isReverse: false,
});
assert.equal(updated.name, 'DHL Express');
assert.equal(updated.hasPackages, true);
console.log('PASS: update modified carrier');
// 9. reorder
const ordered = await store.carrier.order(googleAuthId, {
carrierIds: [c3.id, c2.id, c1.id],
});
assert.deepEqual(ordered.carrierIds, [c3.id, c2.id, c1.id]);
const listAfterOrder = await store.carrier.list(googleAuthId);
assert.equal(listAfterOrder[0].id, c3.id);
assert.equal(listAfterOrder[0].sort, 0);
assert.equal(listAfterOrder[1].id, c2.id);
assert.equal(listAfterOrder[1].sort, 1);
assert.equal(listAfterOrder[2].id, c1.id);
assert.equal(listAfterOrder[2].sort, 2);
console.log('PASS: order successfully reordered carriers');
// 10. delete
await store.carrier.delete(googleAuthId, c3.id);
const listAfterDelete = await store.carrier.list(googleAuthId);
assert.equal(listAfterDelete.length, 2);
console.log('PASS: delete removed carrier');
await assert.rejects(
() => store.carrier.delete(googleAuthId, c3.id),
{ message: 'Carrier not found' },
);
console.log('PASS: delete non-existent carrier rejected');
console.log('--- ALL CARRIER TESTS PASSED! ---');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -54,6 +54,10 @@ async function run() {
name: 'Max Mustermann', name: 'Max Mustermann',
}); });
const carrier = await store.carrier.create(googleAuthId, {
name: 'Standard Carrier',
});
// 2. Setup tracked article and untracked article // 2. Setup tracked article and untracked article
const trackedArt = await store.article.createUnderParent({ const trackedArt = await store.article.createUnderParent({
googleAuthId, googleAuthId,
@@ -117,6 +121,7 @@ async function run() {
}); });
const shippingOpen = await store.shipping.create(googleAuthId, { const shippingOpen = await store.shipping.create(googleAuthId, {
manifestId: shipment.id, manifestId: shipment.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'open', state: 'open',
}); });
@@ -219,6 +224,7 @@ async function run() {
async () => { async () => {
await store.shipping.create(googleAuthId, { await store.shipping.create(googleAuthId, {
manifestId: order.id, manifestId: order.id,
carrierId: carrier.id,
type: 'express', type: 'express',
state: 'shipped', state: 'shipped',
}); });
@@ -251,6 +257,7 @@ async function run() {
const directShipped = await store.shipping.create(googleAuthId, { const directShipped = await store.shipping.create(googleAuthId, {
manifestId: shipment2.id, manifestId: shipment2.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'shipped', state: 'shipped',
}); });

View File

@@ -150,29 +150,47 @@ async function run() {
} }
assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown'); assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown');
// 5. Add packages to Shipment 1 with tracking codes // 5. Add packages to Shipment 1 with tracking codes and carrier
const carrier = await store.carrier.create(googleAuthId, {
name: 'DHL',
hasPackages: true,
isReverse: false,
});
assert.equal(carrier.name, 'DHL');
assert.equal(carrier.hasPackages, true);
assert.equal(carrier.isReverse, false);
const pkg1 = await store.shipping.create(googleAuthId, { const pkg1 = await store.shipping.create(googleAuthId, {
manifestId: shipment1.id, manifestId: shipment1.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'draft', state: 'draft',
trackingCode: 'TRACK-111', trackingCode: 'TRACK-111',
carrier: 'DHL',
}); });
assert.equal(pkg1.trackingCode, 'TRACK-111'); assert.equal(pkg1.trackingCode, 'TRACK-111');
assert.equal(pkg1.carrierId, carrier.id);
assert.equal(pkg1.carrier, 'DHL'); assert.equal(pkg1.carrier, 'DHL');
assert.equal(pkg1.state, 'draft'); assert.equal(pkg1.state, 'draft');
console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode); console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode);
const pkg2 = await store.shipping.create(googleAuthId, { const pkg2 = await store.shipping.create(googleAuthId, {
manifestId: shipment1.id, manifestId: shipment1.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'draft', state: 'draft',
trackingCode: 'TRACK-222', trackingCode: 'TRACK-222',
carrier: 'DHL',
}); });
assert.equal(pkg2.trackingCode, 'TRACK-222'); assert.equal(pkg2.trackingCode, 'TRACK-222');
assert.equal(pkg2.carrierId, carrier.id);
console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode); console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode);
// Verify carrier in use prevents deletion
await assert.rejects(
() => store.carrier.delete(googleAuthId, carrier.id),
{ message: 'Carrier in use' },
);
console.log('PASS: Carrier delete rejected while referenced in shipping');
// Check listByOrderId // Check listByOrderId
const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id); const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id);
assert.equal(orderShipments.length, 2); assert.equal(orderShipments.length, 2);

View File

@@ -165,6 +165,12 @@ import warehouseList from './store/warehouse/list.js';
import warehouseUpdate from './store/warehouse/update.js'; import warehouseUpdate from './store/warehouse/update.js';
import warehouseDelete from './store/warehouse/delete.js'; import warehouseDelete from './store/warehouse/delete.js';
import warehouseOrder from './store/warehouse/order.js'; import warehouseOrder from './store/warehouse/order.js';
import carrierCreate from './store/carrier/create.js';
import carrierGetById from './store/carrier/getById.js';
import carrierList from './store/carrier/list.js';
import carrierUpdate from './store/carrier/update.js';
import carrierDelete from './store/carrier/delete.js';
import carrierOrder from './store/carrier/order.js';
import printerCreate from './store/printer/create.js'; import printerCreate from './store/printer/create.js';
import printerGetById from './store/printer/getById.js'; import printerGetById from './store/printer/getById.js';
import printerList from './store/printer/list.js'; import printerList from './store/printer/list.js';
@@ -559,6 +565,15 @@ export default class LibDashStore {
order: warehouseOrder(provider), order: warehouseOrder(provider),
}; };
this.carrier = {
create: carrierCreate(provider),
getById: carrierGetById(provider),
list: carrierList(provider),
update: carrierUpdate(provider),
delete: carrierDelete(provider),
order: carrierOrder(provider),
};
this.printer = { this.printer = {
create: printerCreate(provider), create: printerCreate(provider),
getById: printerGetById(provider), getById: printerGetById(provider),

View File

@@ -7,6 +7,7 @@ export const TABLES = [
'ordersummary', 'ordersummary',
'package', 'package',
'shipping', 'shipping',
'carrier',
'payment', 'payment',
'account', 'account',
'articleattributevalue', 'articleattributevalue',

View File

@@ -0,0 +1,78 @@
export const LIST_BY_COMPANY = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE company_id = @companyId
ORDER BY sort ASC, name ASC, id ASC;
`;
export const GET_BY_COMPANY_AND_ID = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE id = @id AND company_id = @companyId;
`;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE company_id = @companyId AND name = @name;
`;
export const GET_NEXT_SORT = `
SELECT ISNULL(MAX(sort), -1) + 1 AS sort
FROM carrier
WHERE company_id = @companyId;
`;
export const CREATE = `
INSERT INTO carrier (company_id, name, sort, has_packages, is_reverse)
OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort, INSERTED.has_packages, INSERTED.is_reverse
VALUES (@companyId, @name, @sort, @hasPackages, @isReverse);
`;
export const UPDATE = `
UPDATE carrier
SET name = @name, sort = @sort, has_packages = @hasPackages, is_reverse = @isReverse
OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort, INSERTED.has_packages, INSERTED.is_reverse
WHERE id = @id AND company_id = @companyId;
`;
export const LIST_IDS_BY_COMPANY = `
SELECT id
FROM carrier
WHERE company_id = @companyId;
`;
export const UPDATE_SORT = `
UPDATE carrier
SET sort = @sort
WHERE id = @id AND company_id = @companyId;
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM carrier
WHERE id = @id AND company_id = @companyId;
`;
export const CHECK_SHIPPING_USAGE = `
SELECT TOP 1 id
FROM shipping
WHERE carrier_id = @carrierId;
`;

View File

@@ -56,17 +56,24 @@ WHERE id = @id
AND manifest_id IN (SELECT m.id FROM manifest m WHERE m.company_id = @companyId) AND manifest_id IN (SELECT m.id FROM manifest m WHERE m.company_id = @companyId)
`; `;
export const CHECK_CARRIER = `
SELECT TOP 1 id, name
FROM carrier
WHERE id = @carrierId AND company_id = @companyId;
`;
export const SHIPPING_CREATE = ` export const SHIPPING_CREATE = `
INSERT INTO shipping (manifest_id, [number], tracking_code, carrier, [type], [state]) INSERT INTO shipping (manifest_id, [number], tracking_code, carrier_id, [type], [state])
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state] OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier_id, INSERTED.[type], INSERTED.[state]
VALUES (@manifestId, @number, @trackingCode, @carrier, @type, @state) VALUES (@manifestId, @number, @trackingCode, @carrierId, @type, @state)
`; `;
export const SHIPPING_LIST_BY_MANIFEST = ` export const SHIPPING_LIST_BY_MANIFEST = `
SELECT id, manifest_id, [number], tracking_code, carrier, [type], [state] SELECT s.id, s.manifest_id, s.[number], s.tracking_code, s.carrier_id, c.name AS carrier, s.[type], s.[state]
FROM shipping FROM shipping s
WHERE manifest_id = @manifestId LEFT JOIN carrier c ON c.id = s.carrier_id
ORDER BY id ASC WHERE s.manifest_id = @manifestId
ORDER BY s.id ASC
`; `;
export const SHIPPING_DELETE = ` export const SHIPPING_DELETE = `
@@ -76,16 +83,17 @@ WHERE id = @id
`; `;
export const GET_SHIPPING_BY_ID = ` export const GET_SHIPPING_BY_ID = `
SELECT s.id, s.manifest_id AS manifestId, s.[number], s.tracking_code, s.carrier, s.[type], s.[state] SELECT s.id, s.manifest_id AS manifestId, s.[number], s.tracking_code, s.carrier_id AS carrierId, c.name AS carrier, s.[type], s.[state]
FROM shipping s FROM shipping s
INNER JOIN manifest m ON m.id = s.manifest_id INNER JOIN manifest m ON m.id = s.manifest_id
LEFT JOIN carrier c ON c.id = s.carrier_id
WHERE s.id = @id AND m.company_id = @companyId; WHERE s.id = @id AND m.company_id = @companyId;
`; `;
export const SHIPPING_UPDATE = ` export const SHIPPING_UPDATE = `
UPDATE shipping UPDATE shipping
SET [type] = @type, [state] = @state, tracking_code = @trackingCode, carrier = @carrier SET [type] = @type, [state] = @state, tracking_code = @trackingCode, carrier_id = @carrierId
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state] OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier_id, INSERTED.[type], INSERTED.[state]
WHERE id = @id; WHERE id = @id;
`; `;

View File

@@ -1144,6 +1144,22 @@ BEGIN
END END
`; `;
export const INIT_CARRIER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'carrier')
BEGIN
CREATE TABLE carrier (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_carrier_sort DEFAULT 0,
has_packages BIT NOT NULL CONSTRAINT DF_carrier_has_packages DEFAULT 0,
is_reverse BIT NOT NULL CONSTRAINT DF_carrier_is_reverse DEFAULT 0,
CONSTRAINT FK_carrier_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT UQ_carrier_company_name UNIQUE (company_id, name)
);
END
`;
export const INIT_SHIPPING = ` export const INIT_SHIPPING = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'shipping') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'shipping')
BEGIN BEGIN
@@ -1152,10 +1168,11 @@ BEGIN
manifest_id INT NOT NULL, manifest_id INT NOT NULL,
[number] NVARCHAR(100) NOT NULL, [number] NVARCHAR(100) NOT NULL,
tracking_code NVARCHAR(255) NULL, tracking_code NVARCHAR(255) NULL,
carrier NVARCHAR(100) NULL, carrier_id INT NOT NULL,
[type] NVARCHAR(100) NOT NULL, [type] NVARCHAR(100) NOT NULL,
[state] NVARCHAR(100) NOT NULL, [state] NVARCHAR(100) NOT NULL,
CONSTRAINT FK_shipping_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE CONSTRAINT FK_shipping_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_shipping_carrier FOREIGN KEY (carrier_id) REFERENCES carrier(id) ON DELETE NO ACTION
); );
END END
`; `;
@@ -1461,6 +1478,7 @@ export const SCHEMA_SCRIPTS = [
INIT_MANIFESTATTACHMENT_TRIGGER, INIT_MANIFESTATTACHMENT_TRIGGER,
INIT_ACCOUNT, INIT_ACCOUNT,
INIT_PAYMENT, INIT_PAYMENT,
INIT_CARRIER,
INIT_SHIPPING, INIT_SHIPPING,
INIT_PACKAGE, INIT_PACKAGE,
INIT_ORDERSUMMARY, INIT_ORDERSUMMARY,

View File

@@ -0,0 +1,16 @@
import * as source from '../../mssql/queries/carrier.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'carrier');
export const {
LIST_BY_COMPANY,
GET_BY_COMPANY_AND_ID,
GET_BY_COMPANY_AND_NAME,
GET_NEXT_SORT,
CREATE,
UPDATE,
LIST_IDS_BY_COMPANY,
UPDATE_SORT,
DELETE_BY_COMPANY_AND_ID,
CHECK_SHIPPING_USAGE,
} = queries;

View File

@@ -4,6 +4,7 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'logistics'); const queries = translateQueries(source, 'logistics');
export const { export const {
CHECK_ACCOUNT_COMPANY, CHECK_ACCOUNT_COMPANY,
CHECK_CARRIER,
CHECK_ENTITYADDRESS_COMPANY, CHECK_ENTITYADDRESS_COMPANY,
CHECK_SHIPPING_COMPANY, CHECK_SHIPPING_COMPANY,
ACCOUNT_CREATE, ACCOUNT_CREATE,

View File

@@ -818,16 +818,30 @@ CREATE TABLE IF NOT EXISTS payment (
); );
`; `;
export const INIT_CARRIER = `
CREATE TABLE IF NOT EXISTS carrier (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
name TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
has_packages INTEGER NOT NULL DEFAULT 0,
is_reverse INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
UNIQUE (company_id, name)
);
`;
export const INIT_SHIPPING = ` export const INIT_SHIPPING = `
CREATE TABLE IF NOT EXISTS shipping ( CREATE TABLE IF NOT EXISTS shipping (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
manifest_id INTEGER NOT NULL, manifest_id INTEGER NOT NULL,
"number" TEXT NOT NULL, "number" TEXT NOT NULL,
tracking_code TEXT NULL, tracking_code TEXT NULL,
carrier TEXT NULL, carrier_id INTEGER NOT NULL,
"type" TEXT NOT NULL, "type" TEXT NOT NULL,
"state" TEXT NOT NULL, "state" TEXT NOT NULL,
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
FOREIGN KEY (carrier_id) REFERENCES carrier(id) ON DELETE NO ACTION
); );
`; `;
@@ -1090,6 +1104,7 @@ export const SCHEMA_SCRIPTS = [
INIT_MANIFESTATTACHMENT_TRIGGER, INIT_MANIFESTATTACHMENT_TRIGGER,
INIT_ACCOUNT, INIT_ACCOUNT,
INIT_PAYMENT, INIT_PAYMENT,
INIT_CARRIER,
INIT_SHIPPING, INIT_SHIPPING,
INIT_PACKAGE, INIT_PACKAGE,
INIT_ORDERSUMMARY, INIT_ORDERSUMMARY,

View File

@@ -0,0 +1,97 @@
import {
CREATE as MSSQL_CREATE,
GET_NEXT_SORT as MSSQL_GET_NEXT_SORT,
} from '../../providers/mssql/queries/carrier.js';
import {
CREATE as SQLITE_CREATE,
GET_NEXT_SORT as SQLITE_GET_NEXT_SORT,
} from '../../providers/sqlite3/queries/carrier.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
name: row.name,
sort: row.sort,
hasPackages: Boolean(row.has_packages ?? row.hasPackages),
isReverse: Boolean(row.is_reverse ?? row.isReverse),
};
}
export default function create(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
const sql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE;
/**
* Creates a new carrier for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Carrier input.
* @param {string} input.name - The carrier name.
* @param {number} [input.sort] - Optional sort order (auto-increments if omitted).
* @param {boolean} [input.hasPackages=false] - Whether this carrier uses packages.
* @param {boolean} [input.isReverse=false] - Whether this carrier handles reverse shipments.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>} The created carrier.
* @throws {Error} If name is missing, sort is invalid, or carrier already exists.
*
* @example
* const c = await store.carrier.create(googleAuthId, { name: 'DHL', hasPackages: true });
* // => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
*/
return async (googleAuthId, input = {}, tx) => {
const rawName = input?.name;
const cleaned = typeof rawName === 'string' ? rawName.trim() : '';
if (!cleaned) {
throw new Error('Name required');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
let sort = input.sort;
if (sort === undefined || sort === null) {
const next = await runner.query(getSortSql, { companyId });
sort = next.rows[0]?.sort ?? 0;
} else if (!Number.isFinite(sort)) {
throw new Error('Invalid sort');
}
const hasPackages = Boolean(input.hasPackages ?? input.has_packages);
const isReverse = Boolean(input.isReverse ?? input.is_reverse);
try {
const result = await runner.query(sql, {
companyId,
name: cleaned,
sort,
hasPackages: hasPackages ? 1 : 0,
isReverse: isReverse ? 1 : 0,
});
const row = result.rows[0] ?? {
id: result.insertId,
company_id: companyId,
name: cleaned,
sort,
has_packages: hasPackages ? 1 : 0,
is_reverse: isReverse ? 1 : 0,
};
return shape(row);
} catch (err) {
if (
isUniqueConstraintError(err) ||
err?.number === 2627 ||
err?.number === 2601 ||
err?.code === 'SQLITE_CONSTRAINT_UNIQUE'
) {
throw new Error('Carrier already exists');
}
throw err;
}
};
}

View File

@@ -0,0 +1,62 @@
import {
DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE,
CHECK_SHIPPING_USAGE as MSSQL_CHECK_USAGE,
} from '../../providers/mssql/queries/carrier.js';
import {
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
CHECK_SHIPPING_USAGE as SQLITE_CHECK_USAGE,
} from '../../providers/sqlite3/queries/carrier.js';
import { isForeignKeyConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const checkUsageSql = isSqlite ? SQLITE_CHECK_USAGE : MSSQL_CHECK_USAGE;
/**
* Deletes a carrier for the user's selected company, unless referenced in shipping.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The carrier ID to delete.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<void>}
* @throws {Error} If carrier not found or in use in shipping.
*
* @example
* await store.carrier.delete(googleAuthId, 1);
*/
return async (googleAuthId, id, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Carrier not found');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
// Pre-check if carrier is referenced in shipping
const usage = await runner.query(checkUsageSql, { carrierId: numericId });
if (usage.rows && usage.rows.length > 0) {
throw new Error('Carrier in use');
}
try {
const result = await runner.query(deleteSql, {
id: numericId,
companyId,
});
if (!result.rowsAffected) {
throw new Error('Carrier not found');
}
} catch (err) {
if (isForeignKeyConstraintError(err)) {
throw new Error('Carrier in use');
}
throw err;
}
};
}

View File

@@ -0,0 +1,50 @@
import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/carrier.js';
import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/carrier.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
name: row.name,
sort: row.sort,
hasPackages: Boolean(row.has_packages ?? row.hasPackages),
isReverse: Boolean(row.is_reverse ?? row.isReverse),
};
}
export default function getById(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
/**
* Retrieves a carrier by its ID for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The carrier ID.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>} The carrier or undefined.
*
* @example
* const c = await store.carrier.getById(googleAuthId, 1);
* // => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
*/
return async (googleAuthId, id, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
return undefined;
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(sql, {
id: numericId,
companyId,
});
const row = result.rows[0];
return row ? shape(row) : undefined;
};
}

39
src/store/carrier/list.js Normal file
View File

@@ -0,0 +1,39 @@
import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/carrier.js';
import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/carrier.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
name: row.name,
sort: row.sort,
hasPackages: Boolean(row.has_packages ?? row.hasPackages),
isReverse: Boolean(row.is_reverse ?? row.isReverse),
};
}
export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider);
const sql =
provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST;
/**
* Lists all carriers for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>} List of carriers.
*
* @example
* const carriers = await store.carrier.list(googleAuthId);
* // => [{ id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }]
*/
return async (googleAuthId, tx) => {
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const result = await runner.query(sql, { companyId });
return result.rows.map(shape);
};
}

View File

@@ -0,0 +1,79 @@
import {
LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS_BY_COMPANY,
UPDATE_SORT as MSSQL_UPDATE_SORT,
} from '../../providers/mssql/queries/carrier.js';
import {
LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS_BY_COMPANY,
UPDATE_SORT as SQLITE_UPDATE_SORT,
} from '../../providers/sqlite3/queries/carrier.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
export default function order(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const listIdsSql = isSqlite
? SQLITE_LIST_IDS_BY_COMPANY
: MSSQL_LIST_IDS_BY_COMPANY;
const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT;
/**
* Reorders the carriers of the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - The reorder input.
* @param {Array<number>} input.carrierIds - The carrier ids in their new order.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{carrierIds: Array<number>}>} The applied order.
* @throws {Error} If the order is invalid.
*
* @example
* await store.carrier.order(googleAuthId, { carrierIds: [2, 1, 3] });
*/
return async (googleAuthId, { carrierIds } = {}, tx) => {
if (!Array.isArray(carrierIds)) {
throw new Error('Invalid carrier order');
}
const runOrder = async (runner) => {
const { companyId } = await access(googleAuthId, runner);
const items = await runner.query(listIdsSql, { companyId });
const existing = new Set(items.rows.map((r) => r.id));
if (carrierIds.length !== existing.size) {
throw new Error('Invalid carrier order');
}
const seen = new Set();
for (const carrierId of carrierIds) {
if (
!Number.isFinite(carrierId) ||
!existing.has(carrierId) ||
seen.has(carrierId)
) {
throw new Error('Invalid carrier order');
}
seen.add(carrierId);
}
for (let sort = 0; sort < carrierIds.length; sort++) {
await runner.query(updateSortSql, {
id: carrierIds[sort],
companyId,
sort,
});
}
return { carrierIds };
};
if (tx) {
return runOrder(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runOrder);
}
return runOrder(makeRunner(provider));
};
}

122
src/store/carrier/update.js Normal file
View File

@@ -0,0 +1,122 @@
import {
GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID,
UPDATE as MSSQL_UPDATE,
} from '../../providers/mssql/queries/carrier.js';
import {
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
UPDATE as SQLITE_UPDATE,
} from '../../providers/sqlite3/queries/carrier.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js';
function shape(row) {
return {
id: row.id,
name: row.name,
sort: row.sort,
hasPackages: Boolean(row.has_packages ?? row.hasPackages),
isReverse: Boolean(row.is_reverse ?? row.isReverse),
};
}
export default function update(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE;
/**
* Updates a carrier name, sort order, hasPackages, and/or isReverse for the user's selected company.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} id - The carrier ID to update.
* @param {object} input - Update payload.
* @param {string} [input.name] - The new carrier name.
* @param {number} [input.sort] - The new sort order.
* @param {boolean} [input.hasPackages] - Whether carrier uses packages.
* @param {boolean} [input.isReverse] - Whether carrier handles reverse shipments.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>} The updated carrier.
* @throws {Error} If carrier not found, name is empty, sort is invalid, or carrier already exists.
*
* @example
* const updated = await store.carrier.update(googleAuthId, 1, { name: 'DHL Express', hasPackages: true });
* // => { id: 1, name: 'DHL Express', sort: 0, hasPackages: true, isReverse: false }
*/
return async (googleAuthId, id, input = {}, tx) => {
const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Carrier not found');
}
const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner);
const existing = await runner.query(getSql, { id: numericId, companyId });
const current = existing.rows[0];
if (!current) {
throw new Error('Carrier not found');
}
let name = current.name;
if (input.name !== undefined) {
const cleaned = typeof input.name === 'string' ? input.name.trim() : '';
if (!cleaned) {
throw new Error('Name required');
}
name = cleaned;
}
let sort = current.sort;
if (input.sort !== undefined) {
if (!Number.isFinite(input.sort)) {
throw new Error('Invalid sort');
}
sort = input.sort;
}
const hasPackagesVal = input.hasPackages !== undefined
? input.hasPackages
: input.has_packages;
const hasPackages = hasPackagesVal !== undefined
? Boolean(hasPackagesVal)
: Boolean(current.has_packages ?? current.hasPackages);
const isReverseVal = input.isReverse !== undefined
? input.isReverse
: input.is_reverse;
const isReverse = isReverseVal !== undefined
? Boolean(isReverseVal)
: Boolean(current.is_reverse ?? current.isReverse);
try {
await runner.query(updateSql, {
id: numericId,
companyId,
name,
sort,
hasPackages: hasPackages ? 1 : 0,
isReverse: isReverse ? 1 : 0,
});
return shape({
id: numericId,
name,
sort,
has_packages: hasPackages ? 1 : 0,
is_reverse: isReverse ? 1 : 0,
});
} catch (err) {
if (
isUniqueConstraintError(err) ||
err?.number === 2627 ||
err?.number === 2601 ||
err?.code === 'SQLITE_CONSTRAINT_UNIQUE'
) {
throw new Error('Carrier already exists');
}
throw err;
}
};
}

View File

@@ -28,6 +28,7 @@ export function shapeShipping(row) {
manifestId: row.manifest_id ?? row.manifestId, manifestId: row.manifest_id ?? row.manifestId,
number: row.number, number: row.number,
trackingCode: row.tracking_code ?? row.trackingCode ?? null, trackingCode: row.tracking_code ?? row.trackingCode ?? null,
carrierId: row.carrier_id ?? row.carrierId,
carrier: row.carrier ?? null, carrier: row.carrier ?? null,
type: row.type, type: row.type,
state: row.state, state: row.state,

View File

@@ -28,20 +28,20 @@ const SQL_LINE_STATS = `
const SQL_SHIPPING_STATS_SQLITE = ` const SQL_SHIPPING_STATS_SQLITE = `
SELECT SELECT
COUNT(id) AS package_count, COUNT(s.id) AS package_count,
(SELECT carrier FROM shipping WHERE manifest_id = @manifestId AND carrier IS NOT NULL AND TRIM(carrier) != '' ORDER BY id ASC LIMIT 1) AS carrier, (SELECT c.name FROM shipping s INNER JOIN carrier c ON c.id = s.carrier_id WHERE s.manifest_id = @manifestId AND c.name IS NOT NULL AND TRIM(c.name) != '' ORDER BY s.id ASC LIMIT 1) AS carrier,
(SELECT tracking_code FROM shipping WHERE manifest_id = @manifestId AND tracking_code IS NOT NULL AND TRIM(tracking_code) != '' ORDER BY id ASC LIMIT 1) AS tracking_code (SELECT s.tracking_code FROM shipping s WHERE s.manifest_id = @manifestId AND s.tracking_code IS NOT NULL AND TRIM(s.tracking_code) != '' ORDER BY s.id ASC LIMIT 1) AS tracking_code
FROM shipping FROM shipping s
WHERE manifest_id = @manifestId WHERE s.manifest_id = @manifestId
`; `;
const SQL_SHIPPING_STATS_MSSQL = ` const SQL_SHIPPING_STATS_MSSQL = `
SELECT SELECT
COUNT(id) AS package_count, COUNT(s.id) AS package_count,
(SELECT TOP 1 carrier FROM shipping WHERE manifest_id = @manifestId AND carrier IS NOT NULL AND LTRIM(RTRIM(carrier)) != '' ORDER BY id ASC) AS carrier, (SELECT TOP 1 c.name FROM shipping s INNER JOIN carrier c ON c.id = s.carrier_id WHERE s.manifest_id = @manifestId AND c.name IS NOT NULL AND LTRIM(RTRIM(c.name)) != '' ORDER BY s.id ASC) AS carrier,
(SELECT TOP 1 tracking_code FROM shipping WHERE manifest_id = @manifestId AND tracking_code IS NOT NULL AND LTRIM(RTRIM(tracking_code)) != '' ORDER BY id ASC) AS tracking_code (SELECT TOP 1 s.tracking_code FROM shipping s WHERE s.manifest_id = @manifestId AND s.tracking_code IS NOT NULL AND LTRIM(RTRIM(s.tracking_code)) != '' ORDER BY s.id ASC) AS tracking_code
FROM shipping FROM shipping s
WHERE manifest_id = @manifestId WHERE s.manifest_id = @manifestId
`; `;
const SQL_ORDER_BY_NUMBER_SQLITE = ` const SQL_ORDER_BY_NUMBER_SQLITE = `

View File

@@ -5,13 +5,14 @@ import stockShip from '../stock/ship.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { syncShipmentSummary } from '../shipment/syncSummary.js'; import { syncShipmentSummary } from '../shipment/syncSummary.js';
function shape(row) { function shape(row, carrierName) {
return { return {
id: row.id, id: row.id,
manifestId: row.manifest_id ?? row.manifestId, manifestId: row.manifest_id ?? row.manifestId,
number: row.number, number: row.number,
trackingCode: row.tracking_code ?? row.trackingCode ?? null, trackingCode: row.tracking_code ?? row.trackingCode ?? null,
carrier: row.carrier ?? null, carrierId: row.carrier_id ?? row.carrierId,
carrier: carrierName ?? row.carrier ?? null,
type: row.type, type: row.type,
state: row.state, state: row.state,
}; };
@@ -32,24 +33,24 @@ export default function create(provider) {
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Shipping input. * @param {object} input - Shipping input.
* @param {number} input.manifestId - The shipment manifest id. * @param {number} input.manifestId - The shipment manifest id.
* @param {number} input.carrierId - The carrier id.
* @param {string} input.type - Shipping type. * @param {string} input.type - Shipping type.
* @param {string} input.state - Shipping state ('shipped' or other). * @param {string} input.state - Shipping state ('shipped' or other).
* @param {string} [input.trackingCode] - Carrier tracking code. * @param {string} [input.trackingCode] - Carrier tracking code.
* @param {string} [input.carrier] - Carrier name.
* @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, 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, 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, or input is invalid.
* *
* @example * @example
* await store.shipping.create(googleAuthId, { manifestId: 42, type: 'parcel', state: 'shipped', trackingCode: '1Z999', carrier: 'UPS' }); * await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
*/ */
return async (googleAuthId, input = {}, tx) => { return async (googleAuthId, input = {}, tx) => {
const manifestId = requireId(input.manifestId, 'Manifest'); const manifestId = requireId(input.manifestId, 'Manifest');
const carrierId = requireId(input.carrierId, 'Carrier');
const type = requireText(input.type, 'Shipping type'); const type = requireText(input.type, 'Shipping type');
const state = requireText(input.state, 'Shipping state'); const state = requireText(input.state, 'Shipping state');
const trackingCode = input.trackingCode ? String(input.trackingCode).trim() : null; const trackingCode = input.trackingCode ? String(input.trackingCode).trim() : null;
const carrier = input.carrier ? String(input.carrier).trim() : null;
const isShipped = state.trim().toLowerCase() === 'shipped'; const isShipped = state.trim().toLowerCase() === 'shipped';
const run = async (runner, innerTx) => { const run = async (runner, innerTx) => {
@@ -68,6 +69,16 @@ export default function create(provider) {
throw new Error('Manifest is not a shipment'); throw new Error('Manifest is not a shipment');
} }
// Verify carrier exists and belongs to company
const carrierRes = await runner.query(q.CHECK_CARRIER, {
carrierId,
companyId,
});
const carrierRow = carrierRes.rows[0];
if (!carrierRow) {
throw new Error('Carrier not found');
}
let alreadyShipped = false; let alreadyShipped = false;
if (isShipped) { if (isShipped) {
// Check if shipment was already marked shipped by an existing package // Check if shipment was already marked shipped by an existing package
@@ -83,7 +94,7 @@ export default function create(provider) {
manifestId, manifestId,
number, number,
trackingCode, trackingCode,
carrier, carrierId,
type, type,
state, state,
}); });
@@ -92,7 +103,7 @@ export default function create(provider) {
manifest_id: manifestId, manifest_id: manifestId,
number, number,
tracking_code: trackingCode, tracking_code: trackingCode,
carrier, carrier_id: carrierId,
type, type,
state, state,
}; };
@@ -146,7 +157,7 @@ export default function create(provider) {
await syncShipmentSummary(runner, { companyId, manifestId }); await syncShipmentSummary(runner, { companyId, manifestId });
return shape(row); return shape(row, carrierRow.name);
}; };
if (tx) { if (tx) {

View File

@@ -31,6 +31,7 @@ export default function listByManifestId(provider) {
manifestId: row.manifest_id, manifestId: row.manifest_id,
number: row.number, number: row.number,
trackingCode: row.tracking_code ?? null, trackingCode: row.tracking_code ?? null,
carrierId: row.carrier_id,
carrier: row.carrier ?? null, carrier: row.carrier ?? null,
type: row.type, type: row.type,
state: row.state, state: row.state,

View File

@@ -4,13 +4,14 @@ import stockShip from '../stock/ship.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { syncShipmentSummary } from '../shipment/syncSummary.js'; import { syncShipmentSummary } from '../shipment/syncSummary.js';
function shape(row) { function shape(row, carrierName) {
return { return {
id: row.id, id: row.id,
manifestId: row.manifest_id ?? row.manifestId, manifestId: row.manifest_id ?? row.manifestId,
number: row.number, number: row.number,
trackingCode: row.tracking_code ?? row.trackingCode ?? null, trackingCode: row.tracking_code ?? row.trackingCode ?? null,
carrier: row.carrier ?? null, carrierId: row.carrier_id ?? row.carrierId,
carrier: carrierName ?? row.carrier ?? null,
type: row.type, type: row.type,
state: row.state, state: row.state,
}; };
@@ -37,11 +38,11 @@ export default function update(provider) {
* @param {string} [patch.type] - Shipping type. * @param {string} [patch.type] - Shipping type.
* @param {string} [patch.state] - Shipping state. * @param {string} [patch.state] - Shipping state.
* @param {string} [patch.trackingCode] - Carrier tracking code. * @param {string} [patch.trackingCode] - Carrier tracking code.
* @param {string} [patch.carrier] - Carrier name. * @param {number} [patch.carrierId] - Carrier ID.
* @param {number} [patch.warehouseId] - Warehouse ID if transitioning to 'shipped'. * @param {number} [patch.warehouseId] - Warehouse ID if transitioning to 'shipped'.
* @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, 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 shipping is not found, cannot be undone, or manifest not found. * @throws {Error} If shipping is not found, carrier is not found, cannot be undone, or manifest not found.
*/ */
return async (googleAuthId, id, patch = {}, tx) => { return async (googleAuthId, id, patch = {}, tx) => {
const shippingId = requireId(id, 'Shipping'); const shippingId = requireId(id, 'Shipping');
@@ -63,7 +64,9 @@ export default function update(provider) {
const hasState = Object.prototype.hasOwnProperty.call(patch, 'state'); const hasState = Object.prototype.hasOwnProperty.call(patch, 'state');
const hasType = Object.prototype.hasOwnProperty.call(patch, 'type'); const hasType = Object.prototype.hasOwnProperty.call(patch, 'type');
const hasTrackingCode = Object.prototype.hasOwnProperty.call(patch, 'trackingCode'); const hasTrackingCode = Object.prototype.hasOwnProperty.call(patch, 'trackingCode');
const hasCarrier = Object.prototype.hasOwnProperty.call(patch, 'carrier'); const hasCarrierId =
Object.prototype.hasOwnProperty.call(patch, 'carrierId') ||
Object.prototype.hasOwnProperty.call(patch, 'carrier_id');
if (isCurrentlyShipped) { if (isCurrentlyShipped) {
if ( if (
@@ -83,9 +86,22 @@ export default function update(provider) {
const nextTrackingCode = hasTrackingCode const nextTrackingCode = hasTrackingCode
? (patch.trackingCode ? String(patch.trackingCode).trim() : null) ? (patch.trackingCode ? String(patch.trackingCode).trim() : null)
: (current.tracking_code ?? current.trackingCode ?? null); : (current.tracking_code ?? current.trackingCode ?? null);
const nextCarrier = hasCarrier
? (patch.carrier ? String(patch.carrier).trim() : null) let nextCarrierId = current.carrierId ?? current.carrier_id;
: (current.carrier ?? null); let nextCarrierName = current.carrier ?? null;
if (hasCarrierId) {
const rawCarrierId = patch.carrierId ?? patch.carrier_id;
nextCarrierId = requireId(rawCarrierId, 'Carrier');
const carrierRes = await runner.query(q.CHECK_CARRIER, {
carrierId: nextCarrierId,
companyId,
});
const carrierRow = carrierRes.rows[0];
if (!carrierRow) {
throw new Error('Carrier not found');
}
nextCarrierName = carrierRow.name;
}
const willBeShipped = const willBeShipped =
!isCurrentlyShipped && !isCurrentlyShipped &&
String(nextState).trim().toLowerCase() === 'shipped'; String(nextState).trim().toLowerCase() === 'shipped';
@@ -153,7 +169,7 @@ export default function update(provider) {
type: nextType, type: nextType,
state: nextState, state: nextState,
trackingCode: nextTrackingCode, trackingCode: nextTrackingCode,
carrier: nextCarrier, carrierId: nextCarrierId,
}); });
const updatedRow = updateRes.rows[0] ?? { const updatedRow = updateRes.rows[0] ?? {
@@ -161,14 +177,14 @@ export default function update(provider) {
manifest_id: current.manifestId, manifest_id: current.manifestId,
number: current.number, number: current.number,
tracking_code: nextTrackingCode, tracking_code: nextTrackingCode,
carrier: nextCarrier, carrier_id: nextCarrierId,
type: nextType, type: nextType,
state: nextState, state: nextState,
}; };
await syncShipmentSummary(runner, { companyId, manifestId: current.manifestId }); await syncShipmentSummary(runner, { companyId, manifestId: current.manifestId });
return shape(updatedRow); return shape(updatedRow, nextCarrierName);
}; };
if (tx) { if (tx) {