@managemint-solutions/sdk 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/index.d.ts +1 -1
- package/dist/audit/index.js +14 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +14 -0
- package/dist/documents/content.js +6 -1
- package/dist/documents/document-templates.js +15 -5
- package/dist/documents/documents.d.ts +14 -5
- package/dist/documents/documents.js +14 -2
- package/dist/documents/dto.d.ts +12 -2
- package/dist/documents/dto.js +61 -1
- package/dist/documents/render/html.d.ts +5 -0
- package/dist/documents/render/html.js +10 -3
- package/dist/documents/render/index.d.ts +1 -1
- package/dist/documents/render/index.js +3 -1
- package/dist/documents/render/values.d.ts +12 -5
- package/dist/documents/render/values.js +16 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +32 -3
- package/dist/inventory/dto.d.ts +134 -0
- package/dist/inventory/dto.js +684 -0
- package/dist/inventory/index.d.ts +7 -0
- package/dist/inventory/index.js +30 -0
- package/dist/inventory/items.d.ts +65 -0
- package/dist/inventory/items.js +216 -0
- package/dist/inventory/levels.d.ts +52 -0
- package/dist/inventory/levels.js +157 -0
- package/dist/inventory/locations.d.ts +29 -0
- package/dist/inventory/locations.js +158 -0
- package/dist/inventory/movements.d.ts +36 -0
- package/dist/inventory/movements.js +103 -0
- package/dist/inventory/stocktakes.d.ts +36 -0
- package/dist/inventory/stocktakes.js +70 -0
- package/dist/inventory/transfers.d.ts +73 -0
- package/dist/inventory/transfers.js +134 -0
- package/dist/organizations/dto.d.ts +3 -1
- package/dist/organizations/dto.js +12 -0
- package/dist/organizations/index.js +2 -0
- package/dist/permissions/dto.d.ts +4 -0
- package/dist/permissions/dto.js +20 -0
- package/dist/service-client/index.d.ts +6 -0
- package/dist/service-client/index.js +10 -0
- package/dist/users/paths.d.ts +5 -6
- package/dist/users/paths.js +12 -12
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './dto';
|
|
2
|
+
export { InventoryItemsResource, type InventoryItemRow } from './items';
|
|
3
|
+
export { InventoryLevelsResource, type InventoryLevelRow } from './levels';
|
|
4
|
+
export { InventoryLocationsResource } from './locations';
|
|
5
|
+
export { InventoryMovementsResource, type CreateInventoryMovementRow } from './movements';
|
|
6
|
+
export { InventoryStocktakesResource, type InventoryStocktakeRow } from './stocktakes';
|
|
7
|
+
export { InventoryTransfersResource, type InventoryTransferRow, type UpdateInventoryTransferRow } from './transfers';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.InventoryTransfersResource = exports.InventoryStocktakesResource = exports.InventoryMovementsResource = exports.InventoryLocationsResource = exports.InventoryLevelsResource = exports.InventoryItemsResource = void 0;
|
|
18
|
+
__exportStar(require("./dto"), exports);
|
|
19
|
+
var items_1 = require("./items");
|
|
20
|
+
Object.defineProperty(exports, "InventoryItemsResource", { enumerable: true, get: function () { return items_1.InventoryItemsResource; } });
|
|
21
|
+
var levels_1 = require("./levels");
|
|
22
|
+
Object.defineProperty(exports, "InventoryLevelsResource", { enumerable: true, get: function () { return levels_1.InventoryLevelsResource; } });
|
|
23
|
+
var locations_1 = require("./locations");
|
|
24
|
+
Object.defineProperty(exports, "InventoryLocationsResource", { enumerable: true, get: function () { return locations_1.InventoryLocationsResource; } });
|
|
25
|
+
var movements_1 = require("./movements");
|
|
26
|
+
Object.defineProperty(exports, "InventoryMovementsResource", { enumerable: true, get: function () { return movements_1.InventoryMovementsResource; } });
|
|
27
|
+
var stocktakes_1 = require("./stocktakes");
|
|
28
|
+
Object.defineProperty(exports, "InventoryStocktakesResource", { enumerable: true, get: function () { return stocktakes_1.InventoryStocktakesResource; } });
|
|
29
|
+
var transfers_1 = require("./transfers");
|
|
30
|
+
Object.defineProperty(exports, "InventoryTransfersResource", { enumerable: true, get: function () { return transfers_1.InventoryTransfersResource; } });
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MmsList } from '@managemint-solutions/entities/common';
|
|
2
|
+
import type { InventoryItemEntity, InventoryItemIdentity } from '@managemint-solutions/entities/inventory';
|
|
3
|
+
import type { CreateInventoryItemDto, GetInventoryItemsDto, UpdateInventoryItemDto } from '@managemint-solutions/entities/inventory/dto';
|
|
4
|
+
import { EntityAuditTrail, type AuditActor, type AuditResource } from '../audit';
|
|
5
|
+
import type { TypedSupabaseClient } from '../client';
|
|
6
|
+
/** An item as the list page reads it, before the api folds its levels in. */
|
|
7
|
+
export type InventoryItemRow = {
|
|
8
|
+
mms_id: string;
|
|
9
|
+
created_at: string;
|
|
10
|
+
name: string;
|
|
11
|
+
sku: string;
|
|
12
|
+
group_name: string;
|
|
13
|
+
cost_minor: number;
|
|
14
|
+
price_minor: number | null;
|
|
15
|
+
track_stock: boolean;
|
|
16
|
+
archived: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Storage bucket every item picture lives in. Public-read; writes are policy-scoped by organization. */
|
|
19
|
+
export declare const ITEM_IMAGES_BUCKET = "item-images";
|
|
20
|
+
/** The item as every embed names it. */
|
|
21
|
+
export declare const ITEM_IDENTITY_SELECT: "mms_id, name, sku";
|
|
22
|
+
/**
|
|
23
|
+
* What is kept. The stock figures (on hand, value, below reorder) are not columns here: the
|
|
24
|
+
* api reads the levels alongside and folds them, so this resource is plain CRUD on the item.
|
|
25
|
+
*/
|
|
26
|
+
export declare class InventoryItemsResource {
|
|
27
|
+
private readonly supabase;
|
|
28
|
+
private readonly auth;
|
|
29
|
+
private readonly table;
|
|
30
|
+
readonly audit: EntityAuditTrail;
|
|
31
|
+
constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
|
|
32
|
+
/**
|
|
33
|
+
* A page of items by name. `ids` narrows the page to those items (the api resolves a
|
|
34
|
+
* `below_reorder` filter to item ids first, as it resolves `completed` for leads).
|
|
35
|
+
*/
|
|
36
|
+
list(query: GetInventoryItemsDto & {
|
|
37
|
+
organization_id?: string | null;
|
|
38
|
+
ids?: string[] | null;
|
|
39
|
+
}): Promise<MmsList<InventoryItemRow>>;
|
|
40
|
+
/** The distinct groups in use, for the filter. */
|
|
41
|
+
groups(): Promise<string[]>;
|
|
42
|
+
/** The identities of the given items, for transfer lines and the count form. */
|
|
43
|
+
identities(itemIds: string[]): Promise<InventoryItemIdentity[]>;
|
|
44
|
+
/** The item without its levels; the api attaches them. */
|
|
45
|
+
getById(itemId: string): Promise<Omit<InventoryItemEntity, 'levels'>>;
|
|
46
|
+
create(input: CreateInventoryItemDto): Promise<Omit<InventoryItemEntity, 'levels'>>;
|
|
47
|
+
update(itemId: string, input: UpdateInventoryItemDto): Promise<Omit<InventoryItemEntity, 'levels'>>;
|
|
48
|
+
/** The cost per unit after a priced receipt: the price paid, set by the api. */
|
|
49
|
+
/**
|
|
50
|
+
* Stores the item's picture and puts its public URL on the row (the profile picture's shape:
|
|
51
|
+
* a new key per upload under the CDN's cache, the previous object removed once the new URL
|
|
52
|
+
* is on). The api's upload route is the one caller.
|
|
53
|
+
*/
|
|
54
|
+
setImage(itemId: string, file: File): Promise<void>;
|
|
55
|
+
/** Takes the picture off the row, then out of the bucket. */
|
|
56
|
+
clearImage(itemId: string): Promise<void>;
|
|
57
|
+
setCost(itemId: string, costMinor: number): Promise<void>;
|
|
58
|
+
archive(itemId: string): Promise<Omit<InventoryItemEntity, 'levels'>>;
|
|
59
|
+
unarchive(itemId: string): Promise<Omit<InventoryItemEntity, 'levels'>>;
|
|
60
|
+
/** Hard delete; its levels cascade. The api refuses an item with movements first. */
|
|
61
|
+
remove(itemId: string): Promise<void>;
|
|
62
|
+
/** Every DTO key, the blanks as the columns' defaults. */
|
|
63
|
+
/** The row's own columns; an opening quantity on a create is the api's receipt, not a column. */
|
|
64
|
+
private columns;
|
|
65
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InventoryItemsResource = exports.ITEM_IDENTITY_SELECT = exports.ITEM_IMAGES_BUCKET = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
5
|
+
const audit_1 = require("../audit");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const paths_1 = require("../supporting-files/paths");
|
|
8
|
+
const paths_2 = require("../users/paths");
|
|
9
|
+
const tenancy_1 = require("../tenancy");
|
|
10
|
+
const transforms_1 = require("../transforms");
|
|
11
|
+
/** Storage bucket every item picture lives in. Public-read; writes are policy-scoped by organization. */
|
|
12
|
+
exports.ITEM_IMAGES_BUCKET = 'item-images';
|
|
13
|
+
const ITEM_LIST_SELECT = `
|
|
14
|
+
mms_id,
|
|
15
|
+
created_at,
|
|
16
|
+
name,
|
|
17
|
+
sku,
|
|
18
|
+
group_name,
|
|
19
|
+
cost_minor,
|
|
20
|
+
price_minor,
|
|
21
|
+
track_stock,
|
|
22
|
+
image_url,
|
|
23
|
+
archived
|
|
24
|
+
`;
|
|
25
|
+
const ITEM_DETAIL_SELECT = `
|
|
26
|
+
mms_id,
|
|
27
|
+
created_at,
|
|
28
|
+
created_by:user_profiles!created_by ${transforms_1.USER_PROFILE_EMBED},
|
|
29
|
+
updated_at,
|
|
30
|
+
last_updated_by:user_profiles!last_updated_by ${transforms_1.USER_PROFILE_EMBED},
|
|
31
|
+
name,
|
|
32
|
+
sku,
|
|
33
|
+
description,
|
|
34
|
+
group_name,
|
|
35
|
+
cost_minor,
|
|
36
|
+
price_minor,
|
|
37
|
+
track_stock,
|
|
38
|
+
image_url,
|
|
39
|
+
archived,
|
|
40
|
+
archived_at,
|
|
41
|
+
archived_by:user_profiles!archived_by ${transforms_1.USER_PROFILE_EMBED},
|
|
42
|
+
unarchived_at,
|
|
43
|
+
unarchived_by:user_profiles!unarchived_by ${transforms_1.USER_PROFILE_EMBED}
|
|
44
|
+
`;
|
|
45
|
+
/** The item as every embed names it. */
|
|
46
|
+
exports.ITEM_IDENTITY_SELECT = 'mms_id, name, sku';
|
|
47
|
+
/**
|
|
48
|
+
* What is kept. The stock figures (on hand, value, below reorder) are not columns here: the
|
|
49
|
+
* api reads the levels alongside and folds them, so this resource is plain CRUD on the item.
|
|
50
|
+
*/
|
|
51
|
+
class InventoryItemsResource {
|
|
52
|
+
supabase;
|
|
53
|
+
auth;
|
|
54
|
+
table;
|
|
55
|
+
audit;
|
|
56
|
+
constructor(supabase, auth, audit) {
|
|
57
|
+
this.supabase = supabase;
|
|
58
|
+
this.auth = auth;
|
|
59
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
60
|
+
table: 'inventory_items',
|
|
61
|
+
entityType: enum_1.AuditTrailEntityType.INVENTORY_ITEMS,
|
|
62
|
+
});
|
|
63
|
+
this.audit = new audit_1.EntityAuditTrail(supabase, auth, enum_1.AuditTrailEntityType.INVENTORY_ITEMS);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A page of items by name. `ids` narrows the page to those items (the api resolves a
|
|
67
|
+
* `below_reorder` filter to item ids first, as it resolves `completed` for leads).
|
|
68
|
+
*/
|
|
69
|
+
async list(query) {
|
|
70
|
+
const page = query.page;
|
|
71
|
+
const pageSize = query.page_size;
|
|
72
|
+
const offset = (page - 1) * pageSize;
|
|
73
|
+
const trimmedSearch = query.search?.trim();
|
|
74
|
+
let itemsQuery = (0, tenancy_1.scopeToOrganization)(this.supabase
|
|
75
|
+
.from('inventory_items')
|
|
76
|
+
.select((0, tenancy_1.withOrganization)(ITEM_LIST_SELECT, this.auth), { count: 'estimated' }), this.auth)
|
|
77
|
+
.eq('archived', query.archive)
|
|
78
|
+
.order('name')
|
|
79
|
+
.range(offset, offset + pageSize - 1);
|
|
80
|
+
if (query.organization_id) {
|
|
81
|
+
itemsQuery = itemsQuery.eq('organization_id', query.organization_id);
|
|
82
|
+
}
|
|
83
|
+
if (trimmedSearch) {
|
|
84
|
+
itemsQuery = itemsQuery.or((0, transforms_1.orIlike)(['name', 'sku', 'group_name'], trimmedSearch));
|
|
85
|
+
}
|
|
86
|
+
if (query.group_name) {
|
|
87
|
+
itemsQuery = itemsQuery.eq('group_name', query.group_name);
|
|
88
|
+
}
|
|
89
|
+
if (query.ids) {
|
|
90
|
+
itemsQuery = itemsQuery.in('mms_id', query.ids);
|
|
91
|
+
}
|
|
92
|
+
const { data, error, count } = await itemsQuery;
|
|
93
|
+
if (error)
|
|
94
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
95
|
+
return (0, transforms_1.toMmsList)(data, page, pageSize, count);
|
|
96
|
+
}
|
|
97
|
+
/** The distinct groups in use, for the filter. */
|
|
98
|
+
async groups() {
|
|
99
|
+
const { data, error } = await this.supabase
|
|
100
|
+
.from('inventory_items')
|
|
101
|
+
.select('group_name')
|
|
102
|
+
.eq('organization_id', this.auth.organization_id)
|
|
103
|
+
.neq('group_name', '')
|
|
104
|
+
.order('group_name');
|
|
105
|
+
if (error)
|
|
106
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
107
|
+
return [...new Set((data ?? []).map((row) => row.group_name))];
|
|
108
|
+
}
|
|
109
|
+
/** The identities of the given items, for transfer lines and the count form. */
|
|
110
|
+
async identities(itemIds) {
|
|
111
|
+
if (itemIds.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
const { data, error } = await this.supabase
|
|
114
|
+
.from('inventory_items')
|
|
115
|
+
.select(exports.ITEM_IDENTITY_SELECT)
|
|
116
|
+
.eq('organization_id', this.auth.organization_id)
|
|
117
|
+
.in('mms_id', itemIds);
|
|
118
|
+
if (error)
|
|
119
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
120
|
+
return (data ?? []);
|
|
121
|
+
}
|
|
122
|
+
/** The item without its levels; the api attaches them. */
|
|
123
|
+
async getById(itemId) {
|
|
124
|
+
const { data, error } = await (0, tenancy_1.scopeToOrganization)(this.supabase.from('inventory_items').select((0, tenancy_1.withOrganization)(ITEM_DETAIL_SELECT, this.auth)), this.auth)
|
|
125
|
+
.eq('mms_id', itemId)
|
|
126
|
+
.single();
|
|
127
|
+
if (error)
|
|
128
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
129
|
+
return data;
|
|
130
|
+
}
|
|
131
|
+
async create(input) {
|
|
132
|
+
const row = await this.table.insert({
|
|
133
|
+
...this.columns(input),
|
|
134
|
+
organization_id: this.auth.organization_id,
|
|
135
|
+
created_by: this.auth.mms_id,
|
|
136
|
+
created_at: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
return this.getById(row.mms_id);
|
|
139
|
+
}
|
|
140
|
+
async update(itemId, input) {
|
|
141
|
+
await this.table.update(itemId, {
|
|
142
|
+
...this.columns(input),
|
|
143
|
+
last_updated_by: this.auth.mms_id,
|
|
144
|
+
updated_at: new Date().toISOString(),
|
|
145
|
+
});
|
|
146
|
+
return this.getById(itemId);
|
|
147
|
+
}
|
|
148
|
+
/** The cost per unit after a priced receipt: the price paid, set by the api. */
|
|
149
|
+
/**
|
|
150
|
+
* Stores the item's picture and puts its public URL on the row (the profile picture's shape:
|
|
151
|
+
* a new key per upload under the CDN's cache, the previous object removed once the new URL
|
|
152
|
+
* is on). The api's upload route is the one caller.
|
|
153
|
+
*/
|
|
154
|
+
async setImage(itemId, file) {
|
|
155
|
+
const { name, extension } = (0, paths_1.splitFileName)((0, paths_1.sanitizeFileName)(file.name));
|
|
156
|
+
const path = `${(0, tenancy_1.requireOrganization)(this.auth)}/${itemId}/${Date.now()}-${name}${extension ? `.${extension}` : ''}`;
|
|
157
|
+
const previous = (0, paths_2.publicObjectPath)(exports.ITEM_IMAGES_BUCKET, (await this.getById(itemId)).image_url);
|
|
158
|
+
const { error } = await this.supabase.storage
|
|
159
|
+
.from(exports.ITEM_IMAGES_BUCKET)
|
|
160
|
+
.upload(path, file, { cacheControl: '3600', upsert: true });
|
|
161
|
+
if (error)
|
|
162
|
+
throw (0, errors_1.mapStorageError)(error, `Failed to upload item image: ${file.name}`);
|
|
163
|
+
await this.table.update(itemId, {
|
|
164
|
+
image_url: this.supabase.storage.from(exports.ITEM_IMAGES_BUCKET).getPublicUrl(path).data.publicUrl,
|
|
165
|
+
last_updated_by: this.auth.mms_id,
|
|
166
|
+
updated_at: new Date().toISOString(),
|
|
167
|
+
});
|
|
168
|
+
if (previous && previous !== path) {
|
|
169
|
+
await this.supabase.storage.from(exports.ITEM_IMAGES_BUCKET).remove([previous]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Takes the picture off the row, then out of the bucket. */
|
|
173
|
+
async clearImage(itemId) {
|
|
174
|
+
const previous = (0, paths_2.publicObjectPath)(exports.ITEM_IMAGES_BUCKET, (await this.getById(itemId)).image_url);
|
|
175
|
+
await this.table.update(itemId, {
|
|
176
|
+
image_url: null,
|
|
177
|
+
last_updated_by: this.auth.mms_id,
|
|
178
|
+
updated_at: new Date().toISOString(),
|
|
179
|
+
});
|
|
180
|
+
if (previous)
|
|
181
|
+
await this.supabase.storage.from(exports.ITEM_IMAGES_BUCKET).remove([previous]);
|
|
182
|
+
}
|
|
183
|
+
async setCost(itemId, costMinor) {
|
|
184
|
+
await this.table.update(itemId, {
|
|
185
|
+
cost_minor: costMinor,
|
|
186
|
+
last_updated_by: this.auth.mms_id,
|
|
187
|
+
updated_at: new Date().toISOString(),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async archive(itemId) {
|
|
191
|
+
await this.table.transition(itemId, enum_1.AuditTrailAction.ARCHIVE, (0, transforms_1.archivedPatch)(this.auth.mms_id));
|
|
192
|
+
return this.getById(itemId);
|
|
193
|
+
}
|
|
194
|
+
async unarchive(itemId) {
|
|
195
|
+
await this.table.transition(itemId, enum_1.AuditTrailAction.UNARCHIVE, (0, transforms_1.unarchivedPatch)(this.auth.mms_id));
|
|
196
|
+
return this.getById(itemId);
|
|
197
|
+
}
|
|
198
|
+
/** Hard delete; its levels cascade. The api refuses an item with movements first. */
|
|
199
|
+
async remove(itemId) {
|
|
200
|
+
await this.table.remove(itemId);
|
|
201
|
+
}
|
|
202
|
+
/** Every DTO key, the blanks as the columns' defaults. */
|
|
203
|
+
/** The row's own columns; an opening quantity on a create is the api's receipt, not a column. */
|
|
204
|
+
columns(input) {
|
|
205
|
+
return {
|
|
206
|
+
name: input.name,
|
|
207
|
+
sku: input.sku ?? '',
|
|
208
|
+
description: input.description ?? '',
|
|
209
|
+
group_name: input.group_name ?? '',
|
|
210
|
+
cost_minor: input.cost_minor ?? 0,
|
|
211
|
+
price_minor: input.price_minor ?? null,
|
|
212
|
+
track_stock: input.track_stock ?? true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
exports.InventoryItemsResource = InventoryItemsResource;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InventoryItemIdentity, InventoryLocationIdentity } from '@managemint-solutions/entities/inventory';
|
|
2
|
+
import type { UpdateInventoryLevelDto } from '@managemint-solutions/entities/inventory/dto';
|
|
3
|
+
import type { UserIdentity } from '@managemint-solutions/entities/users';
|
|
4
|
+
import { type AuditActor, type AuditResource } from '../audit';
|
|
5
|
+
import type { TypedSupabaseClient } from '../client';
|
|
6
|
+
/** A level row as read: numeric columns arrive as strings and are made numbers here. */
|
|
7
|
+
export type InventoryLevelRow = {
|
|
8
|
+
mms_id: string;
|
|
9
|
+
item: InventoryItemIdentity & {
|
|
10
|
+
cost_minor: number;
|
|
11
|
+
track_stock: boolean;
|
|
12
|
+
archived: boolean;
|
|
13
|
+
};
|
|
14
|
+
location: InventoryLocationIdentity;
|
|
15
|
+
on_hand: number;
|
|
16
|
+
reorder_point: number | null;
|
|
17
|
+
reorder_quantity: number | null;
|
|
18
|
+
bin: string;
|
|
19
|
+
updated_at: string | null;
|
|
20
|
+
last_updated_by: UserIdentity | null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The balance of one item at one location and its reorder rule. `on_hand` moves only through
|
|
24
|
+
* `applyQuantity` - a compare-and-set the api calls right after it has written the ledger row,
|
|
25
|
+
* so a lost race is a retry, never an overwrite. The reorder rule and the bin are an ordinary
|
|
26
|
+
* audited update.
|
|
27
|
+
*/
|
|
28
|
+
export declare class InventoryLevelsResource {
|
|
29
|
+
private readonly supabase;
|
|
30
|
+
private readonly auth;
|
|
31
|
+
private readonly table;
|
|
32
|
+
constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
|
|
33
|
+
/** The one row for the pair, or null while none exists (which reads as 0 on hand). */
|
|
34
|
+
get(itemId: string, locationId: string): Promise<InventoryLevelRow | null>;
|
|
35
|
+
/** Every level of the given items (the list page's fold), optionally at one location. */
|
|
36
|
+
listForItems(itemIds: string[], locationId?: string | null): Promise<InventoryLevelRow[]>;
|
|
37
|
+
/** Every level at one location, by item name: the count form and the location's figures. */
|
|
38
|
+
listForLocation(locationId: string): Promise<InventoryLevelRow[]>;
|
|
39
|
+
/** Every level in the organization: the hub's figures. */
|
|
40
|
+
listAll(): Promise<InventoryLevelRow[]>;
|
|
41
|
+
/** The levels with a reorder rule (the api compares on hand to it), optionally at one location. */
|
|
42
|
+
listWithReorderPoint(locationId?: string | null): Promise<InventoryLevelRow[]>;
|
|
43
|
+
/** The row for the pair, made at 0 on hand when there is none yet. */
|
|
44
|
+
ensure(itemId: string, locationId: string): Promise<InventoryLevelRow>;
|
|
45
|
+
/** The reorder rule and the bin; `on_hand` is left alone. */
|
|
46
|
+
updateSettings(itemId: string, locationId: string, input: UpdateInventoryLevelDto): Promise<InventoryLevelRow>;
|
|
47
|
+
/**
|
|
48
|
+
* Moves `on_hand` from `expected` to `next` - only while it still reads `expected`. Resolves
|
|
49
|
+
* false when it did not (another movement landed first): the caller re-reads and tries again.
|
|
50
|
+
*/
|
|
51
|
+
applyQuantity(levelId: string, expected: number, next: number): Promise<boolean>;
|
|
52
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InventoryLevelsResource = void 0;
|
|
4
|
+
const enum_1 = require("@managemint-solutions/entities/audit-trail/enum");
|
|
5
|
+
const audit_1 = require("../audit");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const transforms_1 = require("../transforms");
|
|
8
|
+
const items_1 = require("./items");
|
|
9
|
+
const LEVEL_SELECT = `
|
|
10
|
+
mms_id,
|
|
11
|
+
item:inventory_items!item_id (${items_1.ITEM_IDENTITY_SELECT}, cost_minor, track_stock, archived),
|
|
12
|
+
location:inventory_locations!location_id (mms_id, name),
|
|
13
|
+
on_hand,
|
|
14
|
+
reorder_point,
|
|
15
|
+
reorder_quantity,
|
|
16
|
+
bin,
|
|
17
|
+
updated_at,
|
|
18
|
+
last_updated_by:user_profiles!last_updated_by ${transforms_1.USER_PROFILE_EMBED}
|
|
19
|
+
`;
|
|
20
|
+
const toNumber = (value) => Number(value ?? 0);
|
|
21
|
+
const toNullableNumber = (value) => (value === null || value === undefined ? null : Number(value));
|
|
22
|
+
const asRow = (raw) => {
|
|
23
|
+
const row = raw;
|
|
24
|
+
return {
|
|
25
|
+
...row,
|
|
26
|
+
on_hand: toNumber(row.on_hand),
|
|
27
|
+
reorder_point: toNullableNumber(row.reorder_point),
|
|
28
|
+
reorder_quantity: toNullableNumber(row.reorder_quantity),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The balance of one item at one location and its reorder rule. `on_hand` moves only through
|
|
33
|
+
* `applyQuantity` - a compare-and-set the api calls right after it has written the ledger row,
|
|
34
|
+
* so a lost race is a retry, never an overwrite. The reorder rule and the bin are an ordinary
|
|
35
|
+
* audited update.
|
|
36
|
+
*/
|
|
37
|
+
class InventoryLevelsResource {
|
|
38
|
+
supabase;
|
|
39
|
+
auth;
|
|
40
|
+
table;
|
|
41
|
+
constructor(supabase, auth, audit) {
|
|
42
|
+
this.supabase = supabase;
|
|
43
|
+
this.auth = auth;
|
|
44
|
+
this.table = (0, audit_1.auditedTable)(supabase, auth, audit, {
|
|
45
|
+
table: 'inventory_levels',
|
|
46
|
+
entityType: enum_1.AuditTrailEntityType.INVENTORY_LEVELS,
|
|
47
|
+
related: { entityType: enum_1.AuditTrailEntityType.INVENTORY_ITEMS, column: 'item_id' },
|
|
48
|
+
identity: ['item_id', 'location_id'],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** The one row for the pair, or null while none exists (which reads as 0 on hand). */
|
|
52
|
+
async get(itemId, locationId) {
|
|
53
|
+
const { data, error } = await this.supabase
|
|
54
|
+
.from('inventory_levels')
|
|
55
|
+
.select(LEVEL_SELECT)
|
|
56
|
+
.eq('organization_id', this.auth.organization_id)
|
|
57
|
+
.eq('item_id', itemId)
|
|
58
|
+
.eq('location_id', locationId)
|
|
59
|
+
.maybeSingle();
|
|
60
|
+
if (error)
|
|
61
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
62
|
+
return data ? asRow(data) : null;
|
|
63
|
+
}
|
|
64
|
+
/** Every level of the given items (the list page's fold), optionally at one location. */
|
|
65
|
+
async listForItems(itemIds, locationId) {
|
|
66
|
+
if (itemIds.length === 0)
|
|
67
|
+
return [];
|
|
68
|
+
let query = this.supabase
|
|
69
|
+
.from('inventory_levels')
|
|
70
|
+
.select(LEVEL_SELECT)
|
|
71
|
+
.eq('organization_id', this.auth.organization_id)
|
|
72
|
+
.in('item_id', itemIds);
|
|
73
|
+
if (locationId)
|
|
74
|
+
query = query.eq('location_id', locationId);
|
|
75
|
+
const { data, error } = await query;
|
|
76
|
+
if (error)
|
|
77
|
+
throw (0, errors_1.mapPostgrestError)(error);
|
|
78
|
+
return (data ?? []).map(asRow);
|
|
79
|
+
}
|
|
80
|
+
/** Every level at one location, by item name: the count form and the location's figures. */
|
|
81
|
+
async listForLocation(locationId) {
|
|
82
|
+
const rows = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
83
|
+
.from('inventory_levels')
|
|
84
|
+
.select(LEVEL_SELECT)
|
|
85
|
+
.eq('organization_id', this.auth.organization_id)
|
|
86
|
+
.eq('location_id', locationId)
|
|
87
|
+
.order('mms_id')
|
|
88
|
+
.range(from, to));
|
|
89
|
+
return rows.map(asRow);
|
|
90
|
+
}
|
|
91
|
+
/** Every level in the organization: the hub's figures. */
|
|
92
|
+
async listAll() {
|
|
93
|
+
const rows = await (0, transforms_1.allRows)((from, to) => this.supabase
|
|
94
|
+
.from('inventory_levels')
|
|
95
|
+
.select(LEVEL_SELECT)
|
|
96
|
+
.eq('organization_id', this.auth.organization_id)
|
|
97
|
+
.order('mms_id')
|
|
98
|
+
.range(from, to));
|
|
99
|
+
return rows.map(asRow);
|
|
100
|
+
}
|
|
101
|
+
/** The levels with a reorder rule (the api compares on hand to it), optionally at one location. */
|
|
102
|
+
async listWithReorderPoint(locationId) {
|
|
103
|
+
const rows = await (0, transforms_1.allRows)((from, to) => {
|
|
104
|
+
let query = this.supabase
|
|
105
|
+
.from('inventory_levels')
|
|
106
|
+
.select(LEVEL_SELECT)
|
|
107
|
+
.eq('organization_id', this.auth.organization_id)
|
|
108
|
+
.not('reorder_point', 'is', null)
|
|
109
|
+
.order('mms_id')
|
|
110
|
+
.range(from, to);
|
|
111
|
+
if (locationId)
|
|
112
|
+
query = query.eq('location_id', locationId);
|
|
113
|
+
return query;
|
|
114
|
+
});
|
|
115
|
+
return rows.map(asRow);
|
|
116
|
+
}
|
|
117
|
+
/** The row for the pair, made at 0 on hand when there is none yet. */
|
|
118
|
+
async ensure(itemId, locationId) {
|
|
119
|
+
const existing = await this.get(itemId, locationId);
|
|
120
|
+
if (existing)
|
|
121
|
+
return existing;
|
|
122
|
+
const row = await this.table.insert({
|
|
123
|
+
item_id: itemId,
|
|
124
|
+
location_id: locationId,
|
|
125
|
+
on_hand: 0,
|
|
126
|
+
organization_id: this.auth.organization_id,
|
|
127
|
+
last_updated_by: this.auth.mms_id,
|
|
128
|
+
updated_at: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
return (await this.get(row.item_id, row.location_id));
|
|
131
|
+
}
|
|
132
|
+
/** The reorder rule and the bin; `on_hand` is left alone. */
|
|
133
|
+
async updateSettings(itemId, locationId, input) {
|
|
134
|
+
const level = await this.ensure(itemId, locationId);
|
|
135
|
+
await this.table.update(level.mms_id, {
|
|
136
|
+
reorder_point: input.reorder_point,
|
|
137
|
+
reorder_quantity: input.reorder_quantity,
|
|
138
|
+
bin: input.bin ?? '',
|
|
139
|
+
last_updated_by: this.auth.mms_id,
|
|
140
|
+
updated_at: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
return (await this.get(itemId, locationId));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Moves `on_hand` from `expected` to `next` - only while it still reads `expected`. Resolves
|
|
146
|
+
* false when it did not (another movement landed first): the caller re-reads and tries again.
|
|
147
|
+
*/
|
|
148
|
+
async applyQuantity(levelId, expected, next) {
|
|
149
|
+
const row = await this.table.updateWhere(levelId, (query) => query.eq('on_hand', expected), {
|
|
150
|
+
on_hand: next,
|
|
151
|
+
last_updated_by: this.auth.mms_id,
|
|
152
|
+
updated_at: new Date().toISOString(),
|
|
153
|
+
});
|
|
154
|
+
return row !== null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.InventoryLevelsResource = InventoryLevelsResource;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InventoryLocationEntity } from '@managemint-solutions/entities/inventory';
|
|
2
|
+
import type { CreateInventoryLocationDto, UpdateInventoryLocationDto } from '@managemint-solutions/entities/inventory/dto';
|
|
3
|
+
import { EntityAuditTrail, type AuditActor, type AuditResource } from '../audit';
|
|
4
|
+
import type { TypedSupabaseClient } from '../client';
|
|
5
|
+
/**
|
|
6
|
+
* Where stock is kept. A handful of rows per organization, read whole (no paging). One of them
|
|
7
|
+
* is the default: `is_default` moves between rows and is never cleared, so an organization
|
|
8
|
+
* with any location has one (the api stars the first location added; nothing is seeded).
|
|
9
|
+
*/
|
|
10
|
+
export declare class InventoryLocationsResource {
|
|
11
|
+
private readonly supabase;
|
|
12
|
+
private readonly auth;
|
|
13
|
+
private readonly table;
|
|
14
|
+
readonly audit: EntityAuditTrail;
|
|
15
|
+
constructor(supabase: TypedSupabaseClient, auth: AuditActor, audit: AuditResource);
|
|
16
|
+
/** The organization's locations, the default first, then by name. */
|
|
17
|
+
list(archive: boolean): Promise<InventoryLocationEntity[]>;
|
|
18
|
+
getById(locationId: string): Promise<InventoryLocationEntity>;
|
|
19
|
+
/** The default location's id; null while the organization has no location yet. */
|
|
20
|
+
defaultId(): Promise<string | null>;
|
|
21
|
+
create(input: CreateInventoryLocationDto): Promise<InventoryLocationEntity>;
|
|
22
|
+
update(locationId: string, input: UpdateInventoryLocationDto): Promise<InventoryLocationEntity>;
|
|
23
|
+
archive(locationId: string): Promise<InventoryLocationEntity>;
|
|
24
|
+
unarchive(locationId: string): Promise<InventoryLocationEntity>;
|
|
25
|
+
/** Hard delete. The levels' and transfers' `on delete restrict` keys refuse a location still in use (23503 -> 409). */
|
|
26
|
+
remove(locationId: string): Promise<void>;
|
|
27
|
+
/** Takes the star off the current default (other than `except`), so the unique index lets the next one have it. */
|
|
28
|
+
private clearDefault;
|
|
29
|
+
}
|