@voyantjs/suppliers 0.1.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/LICENSE +109 -0
- package/README.md +41 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/routes.d.ts +1183 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +267 -0
- package/dist/schema.d.ts +1040 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +186 -0
- package/dist/service.d.ts +1755 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +602 -0
- package/dist/validation.d.ts +231 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +109 -0
- package/package.json +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { identityAddresses, identityContactPoints, identityNamedContacts, } from "@voyantjs/identity/schema";
|
|
2
|
+
import { identityService } from "@voyantjs/identity/service";
|
|
3
|
+
import { and, asc, desc, eq, gte, inArray, lte, sql } from "drizzle-orm";
|
|
4
|
+
import { supplierAvailability, supplierContracts, supplierNotes, supplierRates, supplierServices, suppliers, } from "./schema.js";
|
|
5
|
+
const supplierEntityType = "supplier";
|
|
6
|
+
const supplierBaseIdentitySource = "suppliers.base";
|
|
7
|
+
const supplierPrimaryNamedContactSource = "suppliers.primary_contact";
|
|
8
|
+
function normalizeContactValue(kind, value) {
|
|
9
|
+
if (kind === "email") {
|
|
10
|
+
return value.trim().toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
if (kind === "website") {
|
|
13
|
+
return value.trim().toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
return value.trim();
|
|
16
|
+
}
|
|
17
|
+
function isManagedBySource(metadata, source) {
|
|
18
|
+
return metadata?.managedBy === source;
|
|
19
|
+
}
|
|
20
|
+
function toNullableTrimmed(value) {
|
|
21
|
+
const trimmed = value?.trim();
|
|
22
|
+
return trimmed ? trimmed : null;
|
|
23
|
+
}
|
|
24
|
+
function formatAddress(address) {
|
|
25
|
+
if (address.fullText) {
|
|
26
|
+
return address.fullText;
|
|
27
|
+
}
|
|
28
|
+
const parts = [
|
|
29
|
+
address.line1,
|
|
30
|
+
address.line2,
|
|
31
|
+
address.city,
|
|
32
|
+
address.region,
|
|
33
|
+
address.postalCode,
|
|
34
|
+
address.country,
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
37
|
+
}
|
|
38
|
+
async function syncSupplierIdentity(db, supplierId, data) {
|
|
39
|
+
const existingContactPoints = await identityService.listContactPointsForEntity(db, supplierEntityType, supplierId);
|
|
40
|
+
const existingAddresses = await identityService.listAddressesForEntity(db, supplierEntityType, supplierId);
|
|
41
|
+
const existingNamedContacts = await identityService.listNamedContactsForEntity(db, supplierEntityType, supplierId);
|
|
42
|
+
const managedContactPoints = existingContactPoints.filter((point) => isManagedBySource(point.metadata, supplierBaseIdentitySource));
|
|
43
|
+
const managedAddress = existingAddresses.find((address) => isManagedBySource(address.metadata, supplierBaseIdentitySource));
|
|
44
|
+
const managedPrimaryContact = existingNamedContacts.find((contact) => isManagedBySource(contact.metadata, supplierPrimaryNamedContactSource));
|
|
45
|
+
for (const [kind, rawValue] of Object.entries({
|
|
46
|
+
email: data.email,
|
|
47
|
+
phone: data.phone,
|
|
48
|
+
website: data.website,
|
|
49
|
+
})) {
|
|
50
|
+
const value = toNullableTrimmed(rawValue);
|
|
51
|
+
const existing = managedContactPoints.find((point) => point.kind === kind) ??
|
|
52
|
+
existingContactPoints.find((point) => point.kind === kind && point.isPrimary);
|
|
53
|
+
if (!value) {
|
|
54
|
+
if (existing) {
|
|
55
|
+
await identityService.deleteContactPoint(db, existing.id);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const payload = {
|
|
60
|
+
entityType: supplierEntityType,
|
|
61
|
+
entityId: supplierId,
|
|
62
|
+
kind,
|
|
63
|
+
label: kind === "website" ? "website" : "primary",
|
|
64
|
+
value,
|
|
65
|
+
normalizedValue: normalizeContactValue(kind, value),
|
|
66
|
+
isPrimary: true,
|
|
67
|
+
metadata: {
|
|
68
|
+
managedBy: supplierBaseIdentitySource,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
if (existing) {
|
|
72
|
+
await identityService.updateContactPoint(db, existing.id, payload);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await identityService.createContactPoint(db, payload);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const addressLine = toNullableTrimmed(data.address);
|
|
79
|
+
const city = toNullableTrimmed(data.city);
|
|
80
|
+
const country = toNullableTrimmed(data.country);
|
|
81
|
+
const hasAddress = Boolean(addressLine || city || country);
|
|
82
|
+
if (!hasAddress) {
|
|
83
|
+
if (managedAddress) {
|
|
84
|
+
await identityService.deleteAddress(db, managedAddress.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const addressPayload = {
|
|
89
|
+
entityType: supplierEntityType,
|
|
90
|
+
entityId: supplierId,
|
|
91
|
+
label: "primary",
|
|
92
|
+
fullText: addressLine,
|
|
93
|
+
line1: addressLine,
|
|
94
|
+
city,
|
|
95
|
+
country,
|
|
96
|
+
isPrimary: true,
|
|
97
|
+
metadata: {
|
|
98
|
+
managedBy: supplierBaseIdentitySource,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
if (managedAddress) {
|
|
102
|
+
await identityService.updateAddress(db, managedAddress.id, addressPayload);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
await identityService.createAddress(db, addressPayload);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const contactName = toNullableTrimmed(data.contactName);
|
|
109
|
+
const contactEmail = toNullableTrimmed(data.contactEmail);
|
|
110
|
+
const contactPhone = toNullableTrimmed(data.contactPhone);
|
|
111
|
+
const hasPrimaryContact = Boolean(contactName || contactEmail || contactPhone);
|
|
112
|
+
if (!hasPrimaryContact) {
|
|
113
|
+
if (managedPrimaryContact) {
|
|
114
|
+
await identityService.deleteNamedContact(db, managedPrimaryContact.id);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const namedContactPayload = {
|
|
119
|
+
entityType: supplierEntityType,
|
|
120
|
+
entityId: supplierId,
|
|
121
|
+
role: "primary",
|
|
122
|
+
name: contactName ?? contactEmail ?? contactPhone ?? "Primary contact",
|
|
123
|
+
email: contactEmail,
|
|
124
|
+
phone: contactPhone,
|
|
125
|
+
isPrimary: true,
|
|
126
|
+
metadata: {
|
|
127
|
+
managedBy: supplierPrimaryNamedContactSource,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
if (managedPrimaryContact) {
|
|
131
|
+
await identityService.updateNamedContact(db, managedPrimaryContact.id, namedContactPayload);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
await identityService.createNamedContact(db, namedContactPayload);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function hydrateSuppliers(db, rows) {
|
|
138
|
+
if (rows.length === 0) {
|
|
139
|
+
return rows.map((row) => ({
|
|
140
|
+
...row,
|
|
141
|
+
email: null,
|
|
142
|
+
phone: null,
|
|
143
|
+
website: null,
|
|
144
|
+
address: null,
|
|
145
|
+
city: null,
|
|
146
|
+
country: null,
|
|
147
|
+
contactName: null,
|
|
148
|
+
contactEmail: null,
|
|
149
|
+
contactPhone: null,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
const ids = rows.map((row) => row.id);
|
|
153
|
+
const [contactPoints, addresses, namedContacts] = await Promise.all([
|
|
154
|
+
db
|
|
155
|
+
.select()
|
|
156
|
+
.from(identityContactPoints)
|
|
157
|
+
.where(and(eq(identityContactPoints.entityType, supplierEntityType), inArray(identityContactPoints.entityId, ids))),
|
|
158
|
+
db
|
|
159
|
+
.select()
|
|
160
|
+
.from(identityAddresses)
|
|
161
|
+
.where(and(eq(identityAddresses.entityType, supplierEntityType), inArray(identityAddresses.entityId, ids))),
|
|
162
|
+
db
|
|
163
|
+
.select()
|
|
164
|
+
.from(identityNamedContacts)
|
|
165
|
+
.where(and(eq(identityNamedContacts.entityType, supplierEntityType), inArray(identityNamedContacts.entityId, ids))),
|
|
166
|
+
]);
|
|
167
|
+
const contactPointMap = new Map();
|
|
168
|
+
const addressMap = new Map();
|
|
169
|
+
const namedContactMap = new Map();
|
|
170
|
+
for (const point of contactPoints) {
|
|
171
|
+
const bucket = contactPointMap.get(point.entityId) ?? [];
|
|
172
|
+
bucket.push(point);
|
|
173
|
+
contactPointMap.set(point.entityId, bucket);
|
|
174
|
+
}
|
|
175
|
+
for (const address of addresses) {
|
|
176
|
+
const bucket = addressMap.get(address.entityId) ?? [];
|
|
177
|
+
bucket.push(address);
|
|
178
|
+
addressMap.set(address.entityId, bucket);
|
|
179
|
+
}
|
|
180
|
+
for (const contact of namedContacts) {
|
|
181
|
+
const bucket = namedContactMap.get(contact.entityId) ?? [];
|
|
182
|
+
bucket.push(contact);
|
|
183
|
+
namedContactMap.set(contact.entityId, bucket);
|
|
184
|
+
}
|
|
185
|
+
return rows.map((row) => {
|
|
186
|
+
const entityContactPoints = contactPointMap.get(row.id) ?? [];
|
|
187
|
+
const entityAddresses = addressMap.get(row.id) ?? [];
|
|
188
|
+
const entityNamedContacts = namedContactMap.get(row.id) ?? [];
|
|
189
|
+
const pickContactValue = (kind) => entityContactPoints.find((point) => point.kind === kind && point.isPrimary)?.value ??
|
|
190
|
+
entityContactPoints.find((point) => point.kind === kind)?.value ??
|
|
191
|
+
null;
|
|
192
|
+
const primaryAddress = entityAddresses.find((address) => address.isPrimary) ?? entityAddresses[0] ?? null;
|
|
193
|
+
const primaryContact = entityNamedContacts.find((contact) => contact.isPrimary) ?? entityNamedContacts[0] ?? null;
|
|
194
|
+
return {
|
|
195
|
+
...row,
|
|
196
|
+
email: pickContactValue("email"),
|
|
197
|
+
phone: pickContactValue("phone"),
|
|
198
|
+
website: pickContactValue("website"),
|
|
199
|
+
address: primaryAddress ? formatAddress(primaryAddress) : null,
|
|
200
|
+
city: primaryAddress?.city ?? null,
|
|
201
|
+
country: primaryAddress?.country ?? null,
|
|
202
|
+
contactName: primaryContact?.name ?? null,
|
|
203
|
+
contactEmail: primaryContact?.email ?? null,
|
|
204
|
+
contactPhone: primaryContact?.phone ?? null,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
export const suppliersService = {
|
|
209
|
+
async listSuppliers(db, query) {
|
|
210
|
+
const conditions = [];
|
|
211
|
+
if (query.type) {
|
|
212
|
+
conditions.push(eq(suppliers.type, query.type));
|
|
213
|
+
}
|
|
214
|
+
if (query.status) {
|
|
215
|
+
conditions.push(eq(suppliers.status, query.status));
|
|
216
|
+
}
|
|
217
|
+
if (query.primaryFacilityId) {
|
|
218
|
+
conditions.push(eq(suppliers.primaryFacilityId, query.primaryFacilityId));
|
|
219
|
+
}
|
|
220
|
+
if (query.search) {
|
|
221
|
+
const term = `%${query.search}%`;
|
|
222
|
+
conditions.push(sql `(
|
|
223
|
+
${suppliers.name} ilike ${term}
|
|
224
|
+
or
|
|
225
|
+
exists (
|
|
226
|
+
select 1
|
|
227
|
+
from ${identityContactPoints}
|
|
228
|
+
where ${identityContactPoints.entityType} = ${supplierEntityType}
|
|
229
|
+
and ${identityContactPoints.entityId} = ${suppliers.id}
|
|
230
|
+
and (
|
|
231
|
+
${identityContactPoints.value} ilike ${term}
|
|
232
|
+
or ${identityContactPoints.normalizedValue} ilike ${term}
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
or exists (
|
|
236
|
+
select 1
|
|
237
|
+
from ${identityNamedContacts}
|
|
238
|
+
where ${identityNamedContacts.entityType} = ${supplierEntityType}
|
|
239
|
+
and ${identityNamedContacts.entityId} = ${suppliers.id}
|
|
240
|
+
and (
|
|
241
|
+
${identityNamedContacts.name} ilike ${term}
|
|
242
|
+
or ${identityNamedContacts.email} ilike ${term}
|
|
243
|
+
or ${identityNamedContacts.phone} ilike ${term}
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
or exists (
|
|
247
|
+
select 1
|
|
248
|
+
from ${identityAddresses}
|
|
249
|
+
where ${identityAddresses.entityType} = ${supplierEntityType}
|
|
250
|
+
and ${identityAddresses.entityId} = ${suppliers.id}
|
|
251
|
+
and (
|
|
252
|
+
${identityAddresses.fullText} ilike ${term}
|
|
253
|
+
or ${identityAddresses.city} ilike ${term}
|
|
254
|
+
or ${identityAddresses.country} ilike ${term}
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
)`);
|
|
258
|
+
}
|
|
259
|
+
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
260
|
+
const [rows, countResult] = await Promise.all([
|
|
261
|
+
db
|
|
262
|
+
.select()
|
|
263
|
+
.from(suppliers)
|
|
264
|
+
.where(where)
|
|
265
|
+
.limit(query.limit)
|
|
266
|
+
.offset(query.offset)
|
|
267
|
+
.orderBy(suppliers.createdAt),
|
|
268
|
+
db.select({ count: sql `count(*)::int` }).from(suppliers).where(where),
|
|
269
|
+
]);
|
|
270
|
+
return {
|
|
271
|
+
data: await hydrateSuppliers(db, rows),
|
|
272
|
+
total: countResult[0]?.count ?? 0,
|
|
273
|
+
limit: query.limit,
|
|
274
|
+
offset: query.offset,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
async getSupplierById(db, id) {
|
|
278
|
+
const [row] = await db.select().from(suppliers).where(eq(suppliers.id, id)).limit(1);
|
|
279
|
+
if (!row) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const [hydrated] = await hydrateSuppliers(db, [row]);
|
|
283
|
+
return hydrated ?? null;
|
|
284
|
+
},
|
|
285
|
+
async createSupplier(db, data) {
|
|
286
|
+
const { email, phone, website, address, city, country, contactName, contactEmail, contactPhone, ...supplierValues } = data;
|
|
287
|
+
const [row] = await db.insert(suppliers).values(supplierValues).returning();
|
|
288
|
+
if (!row) {
|
|
289
|
+
throw new Error("Failed to create supplier");
|
|
290
|
+
}
|
|
291
|
+
await syncSupplierIdentity(db, row.id, {
|
|
292
|
+
email,
|
|
293
|
+
phone,
|
|
294
|
+
website,
|
|
295
|
+
address,
|
|
296
|
+
city,
|
|
297
|
+
country,
|
|
298
|
+
contactName,
|
|
299
|
+
contactEmail,
|
|
300
|
+
contactPhone,
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
...row,
|
|
304
|
+
email: email ?? null,
|
|
305
|
+
phone: phone ?? null,
|
|
306
|
+
website: website ?? null,
|
|
307
|
+
address: address ?? null,
|
|
308
|
+
city: city ?? null,
|
|
309
|
+
country: country ?? null,
|
|
310
|
+
contactName: contactName ?? null,
|
|
311
|
+
contactEmail: contactEmail ?? null,
|
|
312
|
+
contactPhone: contactPhone ?? null,
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
async updateSupplier(db, id, data) {
|
|
316
|
+
const existing = await this.getSupplierById(db, id);
|
|
317
|
+
if (!existing) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const { email, phone, website, address, city, country, contactName, contactEmail, contactPhone, ...supplierValues } = data;
|
|
321
|
+
const [row] = await db
|
|
322
|
+
.update(suppliers)
|
|
323
|
+
.set({ ...supplierValues, updatedAt: new Date() })
|
|
324
|
+
.where(eq(suppliers.id, id))
|
|
325
|
+
.returning();
|
|
326
|
+
if (!row) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
await syncSupplierIdentity(db, id, {
|
|
330
|
+
email: email ?? existing.email,
|
|
331
|
+
phone: phone ?? existing.phone,
|
|
332
|
+
website: website ?? existing.website,
|
|
333
|
+
address: address ?? existing.address,
|
|
334
|
+
city: city ?? existing.city,
|
|
335
|
+
country: country ?? existing.country,
|
|
336
|
+
contactName: contactName ?? existing.contactName,
|
|
337
|
+
contactEmail: contactEmail ?? existing.contactEmail,
|
|
338
|
+
contactPhone: contactPhone ?? existing.contactPhone,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
...row,
|
|
342
|
+
email: email ?? existing.email,
|
|
343
|
+
phone: phone ?? existing.phone,
|
|
344
|
+
website: website ?? existing.website,
|
|
345
|
+
address: address ?? existing.address,
|
|
346
|
+
city: city ?? existing.city,
|
|
347
|
+
country: country ?? existing.country,
|
|
348
|
+
contactName: contactName ?? existing.contactName,
|
|
349
|
+
contactEmail: contactEmail ?? existing.contactEmail,
|
|
350
|
+
contactPhone: contactPhone ?? existing.contactPhone,
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
async deleteSupplier(db, id) {
|
|
354
|
+
const [row] = await db
|
|
355
|
+
.delete(suppliers)
|
|
356
|
+
.where(eq(suppliers.id, id))
|
|
357
|
+
.returning({ id: suppliers.id });
|
|
358
|
+
return row ?? null;
|
|
359
|
+
},
|
|
360
|
+
listContactPoints(db, supplierId) {
|
|
361
|
+
return identityService.listContactPointsForEntity(db, supplierEntityType, supplierId);
|
|
362
|
+
},
|
|
363
|
+
listNamedContacts(db, supplierId) {
|
|
364
|
+
return identityService.listNamedContactsForEntity(db, supplierEntityType, supplierId);
|
|
365
|
+
},
|
|
366
|
+
async createNamedContact(db, supplierId, data) {
|
|
367
|
+
const [supplier] = await db
|
|
368
|
+
.select({ id: suppliers.id })
|
|
369
|
+
.from(suppliers)
|
|
370
|
+
.where(eq(suppliers.id, supplierId))
|
|
371
|
+
.limit(1);
|
|
372
|
+
if (!supplier) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return identityService.createNamedContact(db, {
|
|
376
|
+
...data,
|
|
377
|
+
entityType: supplierEntityType,
|
|
378
|
+
entityId: supplierId,
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
updateNamedContact(db, contactId, data) {
|
|
382
|
+
return identityService.updateNamedContact(db, contactId, data);
|
|
383
|
+
},
|
|
384
|
+
deleteNamedContact(db, contactId) {
|
|
385
|
+
return identityService.deleteNamedContact(db, contactId);
|
|
386
|
+
},
|
|
387
|
+
async createContactPoint(db, supplierId, data) {
|
|
388
|
+
const [supplier] = await db
|
|
389
|
+
.select({ id: suppliers.id })
|
|
390
|
+
.from(suppliers)
|
|
391
|
+
.where(eq(suppliers.id, supplierId))
|
|
392
|
+
.limit(1);
|
|
393
|
+
if (!supplier) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
return identityService.createContactPoint(db, {
|
|
397
|
+
...data,
|
|
398
|
+
entityType: supplierEntityType,
|
|
399
|
+
entityId: supplierId,
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
updateContactPoint(db, contactPointId, data) {
|
|
403
|
+
return identityService.updateContactPoint(db, contactPointId, data);
|
|
404
|
+
},
|
|
405
|
+
deleteContactPoint(db, contactPointId) {
|
|
406
|
+
return identityService.deleteContactPoint(db, contactPointId);
|
|
407
|
+
},
|
|
408
|
+
listAddresses(db, supplierId) {
|
|
409
|
+
return identityService.listAddressesForEntity(db, supplierEntityType, supplierId);
|
|
410
|
+
},
|
|
411
|
+
async createAddress(db, supplierId, data) {
|
|
412
|
+
const [supplier] = await db
|
|
413
|
+
.select({ id: suppliers.id })
|
|
414
|
+
.from(suppliers)
|
|
415
|
+
.where(eq(suppliers.id, supplierId))
|
|
416
|
+
.limit(1);
|
|
417
|
+
if (!supplier) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return identityService.createAddress(db, {
|
|
421
|
+
...data,
|
|
422
|
+
entityType: supplierEntityType,
|
|
423
|
+
entityId: supplierId,
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
updateAddress(db, addressId, data) {
|
|
427
|
+
return identityService.updateAddress(db, addressId, data);
|
|
428
|
+
},
|
|
429
|
+
deleteAddress(db, addressId) {
|
|
430
|
+
return identityService.deleteAddress(db, addressId);
|
|
431
|
+
},
|
|
432
|
+
listServices(db, supplierId) {
|
|
433
|
+
return db
|
|
434
|
+
.select()
|
|
435
|
+
.from(supplierServices)
|
|
436
|
+
.where(eq(supplierServices.supplierId, supplierId))
|
|
437
|
+
.orderBy(supplierServices.createdAt);
|
|
438
|
+
},
|
|
439
|
+
async createService(db, supplierId, data) {
|
|
440
|
+
const [supplier] = await db
|
|
441
|
+
.select({ id: suppliers.id })
|
|
442
|
+
.from(suppliers)
|
|
443
|
+
.where(eq(suppliers.id, supplierId))
|
|
444
|
+
.limit(1);
|
|
445
|
+
if (!supplier) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const [row] = await db
|
|
449
|
+
.insert(supplierServices)
|
|
450
|
+
.values({ ...data, supplierId })
|
|
451
|
+
.returning();
|
|
452
|
+
return row ?? null;
|
|
453
|
+
},
|
|
454
|
+
async updateService(db, serviceId, data) {
|
|
455
|
+
const [row] = await db
|
|
456
|
+
.update(supplierServices)
|
|
457
|
+
.set({ ...data, updatedAt: new Date() })
|
|
458
|
+
.where(eq(supplierServices.id, serviceId))
|
|
459
|
+
.returning();
|
|
460
|
+
return row ?? null;
|
|
461
|
+
},
|
|
462
|
+
async deleteService(db, serviceId) {
|
|
463
|
+
const [row] = await db
|
|
464
|
+
.delete(supplierServices)
|
|
465
|
+
.where(eq(supplierServices.id, serviceId))
|
|
466
|
+
.returning({ id: supplierServices.id });
|
|
467
|
+
return row ?? null;
|
|
468
|
+
},
|
|
469
|
+
listRates(db, serviceId) {
|
|
470
|
+
return db
|
|
471
|
+
.select()
|
|
472
|
+
.from(supplierRates)
|
|
473
|
+
.where(eq(supplierRates.serviceId, serviceId))
|
|
474
|
+
.orderBy(supplierRates.createdAt);
|
|
475
|
+
},
|
|
476
|
+
async createRate(db, serviceId, data) {
|
|
477
|
+
const [service] = await db
|
|
478
|
+
.select({ id: supplierServices.id })
|
|
479
|
+
.from(supplierServices)
|
|
480
|
+
.where(eq(supplierServices.id, serviceId))
|
|
481
|
+
.limit(1);
|
|
482
|
+
if (!service) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const [row] = await db
|
|
486
|
+
.insert(supplierRates)
|
|
487
|
+
.values({ ...data, serviceId })
|
|
488
|
+
.returning();
|
|
489
|
+
return row;
|
|
490
|
+
},
|
|
491
|
+
async updateRate(db, rateId, data) {
|
|
492
|
+
const [row] = await db
|
|
493
|
+
.update(supplierRates)
|
|
494
|
+
.set(data)
|
|
495
|
+
.where(eq(supplierRates.id, rateId))
|
|
496
|
+
.returning();
|
|
497
|
+
return row ?? null;
|
|
498
|
+
},
|
|
499
|
+
async deleteRate(db, rateId) {
|
|
500
|
+
const [row] = await db
|
|
501
|
+
.delete(supplierRates)
|
|
502
|
+
.where(eq(supplierRates.id, rateId))
|
|
503
|
+
.returning({ id: supplierRates.id });
|
|
504
|
+
return row ?? null;
|
|
505
|
+
},
|
|
506
|
+
listNotes(db, supplierId) {
|
|
507
|
+
return db
|
|
508
|
+
.select()
|
|
509
|
+
.from(supplierNotes)
|
|
510
|
+
.where(eq(supplierNotes.supplierId, supplierId))
|
|
511
|
+
.orderBy(supplierNotes.createdAt);
|
|
512
|
+
},
|
|
513
|
+
async createNote(db, supplierId, userId, data) {
|
|
514
|
+
const [supplier] = await db
|
|
515
|
+
.select({ id: suppliers.id })
|
|
516
|
+
.from(suppliers)
|
|
517
|
+
.where(eq(suppliers.id, supplierId))
|
|
518
|
+
.limit(1);
|
|
519
|
+
if (!supplier) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
const [row] = await db
|
|
523
|
+
.insert(supplierNotes)
|
|
524
|
+
.values({
|
|
525
|
+
supplierId,
|
|
526
|
+
authorId: userId,
|
|
527
|
+
content: data.content,
|
|
528
|
+
})
|
|
529
|
+
.returning();
|
|
530
|
+
return row;
|
|
531
|
+
},
|
|
532
|
+
async listAvailability(db, supplierId, query) {
|
|
533
|
+
const conditions = [eq(supplierAvailability.supplierId, supplierId)];
|
|
534
|
+
if (query.from) {
|
|
535
|
+
conditions.push(gte(supplierAvailability.date, query.from));
|
|
536
|
+
}
|
|
537
|
+
if (query.to) {
|
|
538
|
+
conditions.push(lte(supplierAvailability.date, query.to));
|
|
539
|
+
}
|
|
540
|
+
return db
|
|
541
|
+
.select()
|
|
542
|
+
.from(supplierAvailability)
|
|
543
|
+
.where(and(...conditions))
|
|
544
|
+
.orderBy(asc(supplierAvailability.date));
|
|
545
|
+
},
|
|
546
|
+
async createAvailability(db, supplierId, entries) {
|
|
547
|
+
const [supplier] = await db
|
|
548
|
+
.select({ id: suppliers.id })
|
|
549
|
+
.from(suppliers)
|
|
550
|
+
.where(eq(suppliers.id, supplierId))
|
|
551
|
+
.limit(1);
|
|
552
|
+
if (!supplier) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
return db
|
|
556
|
+
.insert(supplierAvailability)
|
|
557
|
+
.values(entries.map((entry) => ({
|
|
558
|
+
supplierId,
|
|
559
|
+
date: entry.date,
|
|
560
|
+
available: entry.available,
|
|
561
|
+
notes: entry.notes ?? null,
|
|
562
|
+
})))
|
|
563
|
+
.returning();
|
|
564
|
+
},
|
|
565
|
+
listContracts(db, supplierId) {
|
|
566
|
+
return db
|
|
567
|
+
.select()
|
|
568
|
+
.from(supplierContracts)
|
|
569
|
+
.where(eq(supplierContracts.supplierId, supplierId))
|
|
570
|
+
.orderBy(desc(supplierContracts.createdAt));
|
|
571
|
+
},
|
|
572
|
+
async createContract(db, supplierId, data) {
|
|
573
|
+
const [supplier] = await db
|
|
574
|
+
.select({ id: suppliers.id })
|
|
575
|
+
.from(suppliers)
|
|
576
|
+
.where(eq(suppliers.id, supplierId))
|
|
577
|
+
.limit(1);
|
|
578
|
+
if (!supplier) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
const [row] = await db
|
|
582
|
+
.insert(supplierContracts)
|
|
583
|
+
.values({ ...data, supplierId })
|
|
584
|
+
.returning();
|
|
585
|
+
return row;
|
|
586
|
+
},
|
|
587
|
+
async updateContract(db, contractId, data) {
|
|
588
|
+
const [row] = await db
|
|
589
|
+
.update(supplierContracts)
|
|
590
|
+
.set({ ...data, updatedAt: new Date() })
|
|
591
|
+
.where(eq(supplierContracts.id, contractId))
|
|
592
|
+
.returning();
|
|
593
|
+
return row ?? null;
|
|
594
|
+
},
|
|
595
|
+
async deleteContract(db, contractId) {
|
|
596
|
+
const [row] = await db
|
|
597
|
+
.delete(supplierContracts)
|
|
598
|
+
.where(eq(supplierContracts.id, contractId))
|
|
599
|
+
.returning({ id: supplierContracts.id });
|
|
600
|
+
return row ?? null;
|
|
601
|
+
},
|
|
602
|
+
};
|