@endora-commerce/mod-quote-requests 0.100.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 +21 -0
- package/README.md +59 -0
- package/dist/admin/index.d.ts +26 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +56 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/RfqCreatePage.d.ts +9 -0
- package/dist/admin/pages/RfqCreatePage.d.ts.map +1 -0
- package/dist/admin/pages/RfqCreatePage.js +163 -0
- package/dist/admin/pages/RfqCreatePage.js.map +1 -0
- package/dist/admin/pages/RfqDetail.d.ts +9 -0
- package/dist/admin/pages/RfqDetail.d.ts.map +1 -0
- package/dist/admin/pages/RfqDetail.js +399 -0
- package/dist/admin/pages/RfqDetail.js.map +1 -0
- package/dist/admin/pages/RfqList.d.ts +9 -0
- package/dist/admin/pages/RfqList.d.ts.map +1 -0
- package/dist/admin/pages/RfqList.js +214 -0
- package/dist/admin/pages/RfqList.js.map +1 -0
- package/dist/admin/validity.d.ts +52 -0
- package/dist/admin/validity.d.ts.map +1 -0
- package/dist/admin/validity.js +61 -0
- package/dist/admin/validity.js.map +1 -0
- package/dist/backend/entities/quote-request-event.entity.d.ts +24 -0
- package/dist/backend/entities/quote-request-event.entity.d.ts.map +1 -0
- package/dist/backend/entities/quote-request-event.entity.js +67 -0
- package/dist/backend/entities/quote-request-event.entity.js.map +1 -0
- package/dist/backend/entities/quote-request-item.entity.d.ts +34 -0
- package/dist/backend/entities/quote-request-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/quote-request-item.entity.js +119 -0
- package/dist/backend/entities/quote-request-item.entity.js.map +1 -0
- package/dist/backend/entities/quote-request-notification-event.entity.d.ts +23 -0
- package/dist/backend/entities/quote-request-notification-event.entity.d.ts.map +1 -0
- package/dist/backend/entities/quote-request-notification-event.entity.js +72 -0
- package/dist/backend/entities/quote-request-notification-event.entity.js.map +1 -0
- package/dist/backend/entities/quote-request-revision.entity.d.ts +35 -0
- package/dist/backend/entities/quote-request-revision.entity.d.ts.map +1 -0
- package/dist/backend/entities/quote-request-revision.entity.js +67 -0
- package/dist/backend/entities/quote-request-revision.entity.js.map +1 -0
- package/dist/backend/entities/quote-request.entity.d.ts +90 -0
- package/dist/backend/entities/quote-request.entity.d.ts.map +1 -0
- package/dist/backend/entities/quote-request.entity.js +191 -0
- package/dist/backend/entities/quote-request.entity.js.map +1 -0
- package/dist/backend/index.d.ts +120 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +361 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +95 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +152 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +13 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +82 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.customer.d.ts +18 -0
- package/dist/backend/routes.customer.d.ts.map +1 -0
- package/dist/backend/routes.customer.js +98 -0
- package/dist/backend/routes.customer.js.map +1 -0
- package/dist/backend/routes.sales-reps.d.ts +12 -0
- package/dist/backend/routes.sales-reps.d.ts.map +1 -0
- package/dist/backend/routes.sales-reps.js +57 -0
- package/dist/backend/routes.sales-reps.js.map +1 -0
- package/dist/backend/services/order-completion-reactor.d.ts +32 -0
- package/dist/backend/services/order-completion-reactor.d.ts.map +1 -0
- package/dist/backend/services/order-completion-reactor.js +57 -0
- package/dist/backend/services/order-completion-reactor.js.map +1 -0
- package/dist/backend/services/quote-request-business-id-generator.d.ts +46 -0
- package/dist/backend/services/quote-request-business-id-generator.d.ts.map +1 -0
- package/dist/backend/services/quote-request-business-id-generator.js +69 -0
- package/dist/backend/services/quote-request-business-id-generator.js.map +1 -0
- package/dist/backend/services/quote-request-read-port.d.ts +36 -0
- package/dist/backend/services/quote-request-read-port.d.ts.map +1 -0
- package/dist/backend/services/quote-request-read-port.js +96 -0
- package/dist/backend/services/quote-request-read-port.js.map +1 -0
- package/dist/backend/services/raised-on-channel.d.ts +23 -0
- package/dist/backend/services/raised-on-channel.d.ts.map +1 -0
- package/dist/backend/services/raised-on-channel.js +26 -0
- package/dist/backend/services/raised-on-channel.js.map +1 -0
- package/dist/backend/services/rfq-admin-service.d.ts +75 -0
- package/dist/backend/services/rfq-admin-service.d.ts.map +1 -0
- package/dist/backend/services/rfq-admin-service.js +487 -0
- package/dist/backend/services/rfq-admin-service.js.map +1 -0
- package/dist/backend/services/rfq-event-service.d.ts +32 -0
- package/dist/backend/services/rfq-event-service.d.ts.map +1 -0
- package/dist/backend/services/rfq-event-service.js +28 -0
- package/dist/backend/services/rfq-event-service.js.map +1 -0
- package/dist/backend/services/rfq-expiry-worker.d.ts +50 -0
- package/dist/backend/services/rfq-expiry-worker.d.ts.map +1 -0
- package/dist/backend/services/rfq-expiry-worker.js +70 -0
- package/dist/backend/services/rfq-expiry-worker.js.map +1 -0
- package/dist/backend/services/rfq-notification-service.d.ts +36 -0
- package/dist/backend/services/rfq-notification-service.d.ts.map +1 -0
- package/dist/backend/services/rfq-notification-service.js +52 -0
- package/dist/backend/services/rfq-notification-service.js.map +1 -0
- package/dist/backend/services/rfq-revision-service.d.ts +70 -0
- package/dist/backend/services/rfq-revision-service.d.ts.map +1 -0
- package/dist/backend/services/rfq-revision-service.js +120 -0
- package/dist/backend/services/rfq-revision-service.js.map +1 -0
- package/dist/backend/services/rfq-service.d.ts +140 -0
- package/dist/backend/services/rfq-service.d.ts.map +1 -0
- package/dist/backend/services/rfq-service.js +786 -0
- package/dist/backend/services/rfq-service.js.map +1 -0
- package/dist/backend/services/sales-channel-attributions.d.ts +38 -0
- package/dist/backend/services/sales-channel-attributions.d.ts.map +1 -0
- package/dist/backend/services/sales-channel-attributions.js +48 -0
- package/dist/backend/services/sales-channel-attributions.js.map +1 -0
- package/dist/manifest.d.ts +203 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +277 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260424T190112_quote_requests_init.d.ts +19 -0
- package/dist/migrations/20260424T190112_quote_requests_init.d.ts.map +1 -0
- package/dist/migrations/20260424T190112_quote_requests_init.js +65 -0
- package/dist/migrations/20260424T190112_quote_requests_init.js.map +1 -0
- package/dist/migrations/20260503T141344_quote_requests_workflow.d.ts +37 -0
- package/dist/migrations/20260503T141344_quote_requests_workflow.d.ts.map +1 -0
- package/dist/migrations/20260503T141344_quote_requests_workflow.js +251 -0
- package/dist/migrations/20260503T141344_quote_requests_workflow.js.map +1 -0
- package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.d.ts +10 -0
- package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.d.ts.map +1 -0
- package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.js +16 -0
- package/dist/migrations/20260611T140415_quote_requests_qr_item_packaging.js.map +1 -0
- package/dist/migrations/20260611T140417_quote_requests_business_id.d.ts +24 -0
- package/dist/migrations/20260611T140417_quote_requests_business_id.d.ts.map +1 -0
- package/dist/migrations/20260611T140417_quote_requests_business_id.js +34 -0
- package/dist/migrations/20260611T140417_quote_requests_business_id.js.map +1 -0
- package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.d.ts +21 -0
- package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.d.ts.map +1 -0
- package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.js +29 -0
- package/dist/migrations/20260617T095510_quote_requests_backfill_admin_created_awaiting.js.map +1 -0
- package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.d.ts +12 -0
- package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.d.ts.map +1 -0
- package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.js +16 -0
- package/dist/migrations/20260718T200342_quote_requests_quote_request_custom_field_values.js.map +1 -0
- package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.d.ts +34 -0
- package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.d.ts.map +1 -0
- package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.js +57 -0
- package/dist/migrations/20260912T125614_quote_requests_quote_request_channel_attribution.js.map +1 -0
- package/dist/migrations/index.d.ts +53 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +61 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/status-mapping.d.ts +18 -0
- package/dist/migrations/status-mapping.d.ts.map +1 -0
- package/dist/migrations/status-mapping.js +28 -0
- package/dist/migrations/status-mapping.js.map +1 -0
- package/docs/quote_requests.md +168 -0
- package/i18n/en.json +14 -0
- package/i18n/pl.json +14 -0
- package/package.json +99 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { RfqAdminService, AdminContext } from './services/rfq-admin-service.js';
|
|
3
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
4
|
+
export interface AdminContextResolver {
|
|
5
|
+
(request: FastifyRequest): Promise<AdminContext>;
|
|
6
|
+
}
|
|
7
|
+
export interface QuoteRequestsAdminDeps {
|
|
8
|
+
adminService: RfqAdminService;
|
|
9
|
+
requireAdmin: RequireAdminFactory;
|
|
10
|
+
resolveAdminContext: AdminContextResolver;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerQuoteRequestsAdminRoutes(app: FastifyInstance, deps: QuoteRequestsAdminDeps): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AAS7E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAwB,MAAM,iCAAiC,CAAC;AAC3G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,eAAe,CAAC;IAC9B,YAAY,EAAE,mBAAmB,CAAC;IAClC,mBAAmB,EAAE,oBAAoB,CAAC;CAC3C;AAcD,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqGf"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { adminApproveQuoteRequestSchema, adminAssignQuoteRequestSchema, adminCancelQuoteRequestSchema, adminCreateQuoteRequestSchema, adminPatchQuoteRequestSchema, rfqStatusSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
function parseIfMatch(request) {
|
|
3
|
+
const header = request.headers['if-match'];
|
|
4
|
+
if (!header || typeof header !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
const trimmed = header.replace(/^"|"$/g, '');
|
|
7
|
+
const n = Number.parseInt(trimmed, 10);
|
|
8
|
+
return Number.isFinite(n) ? n : null;
|
|
9
|
+
}
|
|
10
|
+
function setEtag(reply, version) {
|
|
11
|
+
reply.header('etag', `"${version}"`);
|
|
12
|
+
}
|
|
13
|
+
export async function registerQuoteRequestsAdminRoutes(app, deps) {
|
|
14
|
+
const { adminService, requireAdmin, resolveAdminContext } = deps;
|
|
15
|
+
const guard = requireAdmin('rfqs:handle');
|
|
16
|
+
app.get('/api/v1/admin/quote-requests', { preHandler: guard }, async (request) => {
|
|
17
|
+
const ctx = await resolveAdminContext(request);
|
|
18
|
+
const q = (request.query ?? {});
|
|
19
|
+
const status = q['status']
|
|
20
|
+
? q['status']
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter((s) => rfqStatusSchema.safeParse(s).success)
|
|
24
|
+
: undefined;
|
|
25
|
+
const scope = q['assignmentScope'] ?? undefined;
|
|
26
|
+
const data = await adminService.listAll(ctx, {
|
|
27
|
+
...(q['organizationId'] ? { organizationId: q['organizationId'] } : {}),
|
|
28
|
+
...(status && status.length > 0 ? { status: status } : {}),
|
|
29
|
+
...(scope ? { scope } : {}),
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
data,
|
|
33
|
+
pagination: { limit: 50, nextCursor: null, hasMore: false },
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
app.get('/api/v1/admin/quote-requests/:id', { preHandler: guard }, async (request, reply) => {
|
|
37
|
+
const ctx = await resolveAdminContext(request);
|
|
38
|
+
const rfq = await adminService.getById(ctx, request.params.id);
|
|
39
|
+
setEtag(reply, rfq.version);
|
|
40
|
+
return { data: rfq };
|
|
41
|
+
});
|
|
42
|
+
app.post('/api/v1/admin/quote-requests', { preHandler: guard, schema: { body: adminCreateQuoteRequestSchema } }, async (request, reply) => {
|
|
43
|
+
const ctx = await resolveAdminContext(request);
|
|
44
|
+
const body = adminCreateQuoteRequestSchema.parse(request.body);
|
|
45
|
+
const rfq = await adminService.createOnBehalf(ctx, body);
|
|
46
|
+
setEtag(reply, rfq.version);
|
|
47
|
+
reply.code(201);
|
|
48
|
+
return { data: rfq };
|
|
49
|
+
});
|
|
50
|
+
app.patch('/api/v1/admin/quote-requests/:id', { preHandler: guard, schema: { body: adminPatchQuoteRequestSchema } }, async (request, reply) => {
|
|
51
|
+
const ctx = await resolveAdminContext(request);
|
|
52
|
+
const body = adminPatchQuoteRequestSchema.parse(request.body);
|
|
53
|
+
const expectedVersion = parseIfMatch(request);
|
|
54
|
+
const rfq = await adminService.modify(ctx, request.params.id, expectedVersion, body);
|
|
55
|
+
setEtag(reply, rfq.version);
|
|
56
|
+
return { data: rfq };
|
|
57
|
+
});
|
|
58
|
+
app.post('/api/v1/admin/quote-requests/:id/approve', { preHandler: guard, schema: { body: adminApproveQuoteRequestSchema.optional() } }, async (request, reply) => {
|
|
59
|
+
const ctx = await resolveAdminContext(request);
|
|
60
|
+
const body = adminApproveQuoteRequestSchema.parse(request.body ?? {});
|
|
61
|
+
const expectedVersion = parseIfMatch(request);
|
|
62
|
+
const rfq = await adminService.approve(ctx, request.params.id, expectedVersion, body.note);
|
|
63
|
+
setEtag(reply, rfq.version);
|
|
64
|
+
return { data: rfq };
|
|
65
|
+
});
|
|
66
|
+
app.post('/api/v1/admin/quote-requests/:id/cancel', { preHandler: guard, schema: { body: adminCancelQuoteRequestSchema.optional() } }, async (request, reply) => {
|
|
67
|
+
const ctx = await resolveAdminContext(request);
|
|
68
|
+
const body = adminCancelQuoteRequestSchema.parse(request.body ?? {});
|
|
69
|
+
const expectedVersion = parseIfMatch(request);
|
|
70
|
+
const rfq = await adminService.cancel(ctx, request.params.id, expectedVersion, body.reason);
|
|
71
|
+
setEtag(reply, rfq.version);
|
|
72
|
+
return { data: rfq };
|
|
73
|
+
});
|
|
74
|
+
app.post('/api/v1/admin/quote-requests/:id/assign', { preHandler: guard, schema: { body: adminAssignQuoteRequestSchema } }, async (request, reply) => {
|
|
75
|
+
const ctx = await resolveAdminContext(request);
|
|
76
|
+
const body = adminAssignQuoteRequestSchema.parse(request.body);
|
|
77
|
+
const rfq = await adminService.assign(ctx, request.params.id, body.adminUserId);
|
|
78
|
+
setEtag(reply, rfq.version);
|
|
79
|
+
return { data: rfq };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import type { RfqService, CustomerContext } from './services/rfq-service.js';
|
|
3
|
+
export type RequireCustomerGuard = (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
4
|
+
export interface CustomerContextResolver {
|
|
5
|
+
(request: FastifyRequest): Promise<CustomerContext>;
|
|
6
|
+
}
|
|
7
|
+
export interface QuoteRequestsCustomerDeps {
|
|
8
|
+
rfqService: RfqService;
|
|
9
|
+
requireCustomer: RequireCustomerGuard;
|
|
10
|
+
resolveCustomerContext: CustomerContextResolver;
|
|
11
|
+
/**
|
|
12
|
+
* Optional gate — refuses submission when the Customer's Organization
|
|
13
|
+
* is not `active` (feature 026 US1 / US3).
|
|
14
|
+
*/
|
|
15
|
+
assertOrganizationCanTransact?: (organizationId: string) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function registerQuoteRequestsCustomerRoutes(app: FastifyInstance, deps: QuoteRequestsCustomerDeps): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=routes.customer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.customer.d.ts","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAU7E,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG7E,MAAM,MAAM,oBAAoB,GAAG,CACjC,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,WAAW,uBAAuB;IACtC,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,eAAe,EAAE,oBAAoB,CAAC;IACtC,sBAAsB,EAAE,uBAAuB,CAAC;IAChD;;;OAGG;IACH,6BAA6B,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAcD,wBAAsB,mCAAmC,CACvD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAoIf"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createQuoteRequestSchema, patchQuoteRequestSchema, acceptRevisionSchema, rejectRevisionSchema, resubmitQuoteRequestSchema, ERROR_CODES, OrganizationCannotTransactError, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
function parseIfMatch(request) {
|
|
4
|
+
const header = request.headers['if-match'];
|
|
5
|
+
if (!header || typeof header !== 'string')
|
|
6
|
+
return null;
|
|
7
|
+
const trimmed = header.replace(/^"|"$/g, '');
|
|
8
|
+
const n = Number.parseInt(trimmed, 10);
|
|
9
|
+
return Number.isFinite(n) ? n : null;
|
|
10
|
+
}
|
|
11
|
+
function setEtag(reply, version) {
|
|
12
|
+
reply.header('etag', `"${version}"`);
|
|
13
|
+
}
|
|
14
|
+
export async function registerQuoteRequestsCustomerRoutes(app, deps) {
|
|
15
|
+
const { rfqService, requireCustomer, resolveCustomerContext } = deps;
|
|
16
|
+
const { assertOrganizationCanTransact } = deps;
|
|
17
|
+
const guardCanTransact = async (organizationId) => {
|
|
18
|
+
if (!assertOrganizationCanTransact)
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
await assertOrganizationCanTransact(organizationId);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof OrganizationCannotTransactError) {
|
|
25
|
+
throw new HttpError(423, ERROR_CODES.FORBIDDEN, 'Your Organization cannot submit Quote Requests in its current status.', { code: 'organization_cannot_transact', status: err.status });
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// GET /api/v1/quote-requests — list visible Quote Requests for the caller
|
|
31
|
+
app.get('/api/v1/quote-requests', { preHandler: requireCustomer }, async (request) => {
|
|
32
|
+
const ctx = await resolveCustomerContext(request);
|
|
33
|
+
const data = await rfqService.listForCustomer(ctx);
|
|
34
|
+
return {
|
|
35
|
+
data,
|
|
36
|
+
pagination: { limit: 50, nextCursor: null, hasMore: false },
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
// GET /api/v1/quote-requests/:id — detail
|
|
40
|
+
app.get('/api/v1/quote-requests/:id', { preHandler: requireCustomer }, async (request, reply) => {
|
|
41
|
+
const ctx = await resolveCustomerContext(request);
|
|
42
|
+
const rfq = await rfqService.getForCustomer(request.params.id, ctx);
|
|
43
|
+
setEtag(reply, rfq.version);
|
|
44
|
+
return { data: rfq };
|
|
45
|
+
});
|
|
46
|
+
// POST /api/v1/quote-requests — create + submit a new Quote Request
|
|
47
|
+
app.post('/api/v1/quote-requests', { preHandler: requireCustomer, schema: { body: createQuoteRequestSchema } }, async (request, reply) => {
|
|
48
|
+
const ctx = await resolveCustomerContext(request);
|
|
49
|
+
await guardCanTransact(ctx.organizationId);
|
|
50
|
+
const body = createQuoteRequestSchema.parse(request.body);
|
|
51
|
+
const rfq = await rfqService.createForCustomer(ctx, body);
|
|
52
|
+
setEtag(reply, rfq.version);
|
|
53
|
+
reply.code(201);
|
|
54
|
+
return { data: rfq };
|
|
55
|
+
});
|
|
56
|
+
// PATCH /api/v1/quote-requests/:id — customer-side draft edit
|
|
57
|
+
app.patch('/api/v1/quote-requests/:id', { preHandler: requireCustomer, schema: { body: patchQuoteRequestSchema } }, async (request, reply) => {
|
|
58
|
+
const ctx = await resolveCustomerContext(request);
|
|
59
|
+
const body = patchQuoteRequestSchema.parse(request.body);
|
|
60
|
+
const expectedVersion = parseIfMatch(request);
|
|
61
|
+
const rfq = await rfqService.patchDraft(request.params.id, ctx, body, expectedVersion);
|
|
62
|
+
setEtag(reply, rfq.version);
|
|
63
|
+
return { data: rfq };
|
|
64
|
+
});
|
|
65
|
+
// POST /api/v1/quote-requests/:id/accept-revision
|
|
66
|
+
app.post('/api/v1/quote-requests/:id/accept-revision', { preHandler: requireCustomer, schema: { body: acceptRevisionSchema } }, async (request, reply) => {
|
|
67
|
+
const ctx = await resolveCustomerContext(request);
|
|
68
|
+
const body = acceptRevisionSchema.parse(request.body);
|
|
69
|
+
const rfq = await rfqService.acceptRevision(request.params.id, ctx, body.expectedRevisionNumber);
|
|
70
|
+
setEtag(reply, rfq.version);
|
|
71
|
+
return { data: rfq };
|
|
72
|
+
});
|
|
73
|
+
// POST /api/v1/quote-requests/:id/reject-revision
|
|
74
|
+
app.post('/api/v1/quote-requests/:id/reject-revision', { preHandler: requireCustomer, schema: { body: rejectRevisionSchema } }, async (request, reply) => {
|
|
75
|
+
const ctx = await resolveCustomerContext(request);
|
|
76
|
+
const body = rejectRevisionSchema.parse(request.body);
|
|
77
|
+
const rfq = await rfqService.rejectRevision(request.params.id, ctx, body.expectedRevisionNumber, body.reason);
|
|
78
|
+
setEtag(reply, rfq.version);
|
|
79
|
+
return { data: rfq };
|
|
80
|
+
});
|
|
81
|
+
// POST /api/v1/quote-requests/:id/convert-to-order — start checkout
|
|
82
|
+
app.post('/api/v1/quote-requests/:id/convert-to-order', { preHandler: requireCustomer }, async (request, reply) => {
|
|
83
|
+
const ctx = await resolveCustomerContext(request);
|
|
84
|
+
const result = await rfqService.convertToOrder(request.params.id, ctx);
|
|
85
|
+
reply.code(200);
|
|
86
|
+
return { data: result };
|
|
87
|
+
});
|
|
88
|
+
// POST /api/v1/quote-requests/:id/resubmit — clone into a new Pending RFQ
|
|
89
|
+
app.post('/api/v1/quote-requests/:id/resubmit', { preHandler: requireCustomer, schema: { body: resubmitQuoteRequestSchema.optional() } }, async (request, reply) => {
|
|
90
|
+
const ctx = await resolveCustomerContext(request);
|
|
91
|
+
const body = resubmitQuoteRequestSchema.parse(request.body ?? {});
|
|
92
|
+
const rfq = await rfqService.resubmit(request.params.id, ctx, body);
|
|
93
|
+
setEtag(reply, rfq.version);
|
|
94
|
+
reply.code(201);
|
|
95
|
+
return { data: rfq };
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=routes.customer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.customer.js","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
import type { OrganizationDetailsPort, SalesRepAssignmentPort } from '@endora-commerce/contracts';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
export interface QuoteRequestsSalesRepRoutesDeps {
|
|
6
|
+
emFactory: () => EntityManager;
|
|
7
|
+
requireAdmin: RequireAdminFactory;
|
|
8
|
+
salesRepAssignment: SalesRepAssignmentPort;
|
|
9
|
+
organizations: OrganizationDetailsPort;
|
|
10
|
+
}
|
|
11
|
+
export declare function registerQuoteRequestsSalesRepRoutes(app: FastifyInstance, deps: QuoteRequestsSalesRepRoutesDeps): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=routes.sales-reps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.sales-reps.d.ts","sourceRoot":"","sources":["../../src/backend/routes.sales-reps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAgC5E,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;IAClC,kBAAkB,EAAE,sBAAsB,CAAC;IAC3C,aAAa,EAAE,uBAAuB,CAAC;CACxC;AAED,wBAAsB,mCAAmC,CACvD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,+BAA+B,GACpC,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { QuoteRequest } from './entities/quote-request.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* `GET /api/v1/admin/sales-reps/:adminUserId/organizations` — the reverse side
|
|
4
|
+
* of the sales-rep assignment relation, with the open quote-request count per
|
|
5
|
+
* organisation (feature 008 / T077; moved here by D-166).
|
|
6
|
+
*
|
|
7
|
+
* It sat in `organizations/routes.sales-reps.ts` with the three assignment
|
|
8
|
+
* endpoints, in a file `quote_requests` registered through a dynamic
|
|
9
|
+
* `import()`. That arrangement is what two of `organizations`' three
|
|
10
|
+
* cross-module-import ledger entries described, and this is the endpoint that
|
|
11
|
+
* made one of them: the open-RFQ count is **this** module's fact, so counting
|
|
12
|
+
* it there meant `organizations` naming `quote_requests`' entity, and repairing
|
|
13
|
+
* it in place by resolving `quoteRequestReadPort` from `organizations` would
|
|
14
|
+
* have closed a manifest cycle (`quote_requests` already declares
|
|
15
|
+
* `organizations`) for a question `organizations` does not own.
|
|
16
|
+
*
|
|
17
|
+
* Here it is built from two published ports and this module's own entity, and
|
|
18
|
+
* it names no file in another module's directory at all. It stays gated
|
|
19
|
+
* `rfqs:handle`, and that is the point of the split: the code is declared
|
|
20
|
+
* `module: 'quote_requests'`, so it disappears from `/admin-roles` when this
|
|
21
|
+
* module is switched off — which is correct precisely because this route
|
|
22
|
+
* disappears with it. The three endpoints that must **not** disappear are
|
|
23
|
+
* `organizations`', under `organizations:assign-sales-rep`.
|
|
24
|
+
*
|
|
25
|
+
* The statuses counted as "open" are the three the RFQ workflow treats as live;
|
|
26
|
+
* the list is the one this endpoint has always used.
|
|
27
|
+
*/
|
|
28
|
+
/** The statuses a quote request is counted under while it is still live. */
|
|
29
|
+
const OPEN_STATUSES = ['Pending', 'Created from admin', 'Approved'];
|
|
30
|
+
export async function registerQuoteRequestsSalesRepRoutes(app, deps) {
|
|
31
|
+
const { emFactory, requireAdmin, salesRepAssignment, organizations } = deps;
|
|
32
|
+
const guard = requireAdmin('rfqs:handle');
|
|
33
|
+
app.get('/api/v1/admin/sales-reps/:adminUserId/organizations', { preHandler: guard }, async (request) => {
|
|
34
|
+
const orgIds = await salesRepAssignment.listAssignedOrganizationIds(request.params.adminUserId);
|
|
35
|
+
if (orgIds.length === 0)
|
|
36
|
+
return { data: [] };
|
|
37
|
+
const orgs = await organizations.findByIds(orgIds);
|
|
38
|
+
const em = emFactory();
|
|
39
|
+
const open = await em.find(QuoteRequest, {
|
|
40
|
+
organizationId: { $in: orgIds },
|
|
41
|
+
status: { $in: [...OPEN_STATUSES] },
|
|
42
|
+
});
|
|
43
|
+
const countByOrg = new Map();
|
|
44
|
+
for (const r of open) {
|
|
45
|
+
countByOrg.set(r.organizationId, (countByOrg.get(r.organizationId) ?? 0) + 1);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
data: orgs.map((o) => ({
|
|
49
|
+
organizationId: o.id,
|
|
50
|
+
name: o.name,
|
|
51
|
+
openRfqCount: countByOrg.get(o.id) ?? 0,
|
|
52
|
+
assignedAt: o.createdAt.toISOString(),
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=routes.sales-reps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.sales-reps.js","sourceRoot":"","sources":["../../src/backend/routes.sales-reps.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,4EAA4E;AAC5E,MAAM,aAAa,GAAG,CAAC,SAAS,EAAE,oBAAoB,EAAE,UAAU,CAAU,CAAC;AAS7E,MAAM,CAAC,KAAK,UAAU,mCAAmC,CACvD,GAAoB,EACpB,IAAqC;IAErC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IAC5E,MAAM,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,CAAC;IAE1C,GAAG,CAAC,GAAG,CACL,qDAAqD,EACrD,EAAE,UAAU,EAAE,KAAK,EAAE,EACrB,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,2BAA2B,CACjE,OAAO,CAAC,MAAM,CAAC,WAAW,CAC3B,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAE7C,MAAM,IAAI,GAAG,MAAM,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACnD,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE;YACvC,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;YAC/B,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC,EAAE;SACpC,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC7C,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChF,CAAC;QACD,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACrB,cAAc,EAAE,CAAC,CAAC,EAAE;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC;gBACvC,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;aACtC,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { OrderReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { RfqEventService } from './rfq-event-service.js';
|
|
4
|
+
import type { RfqNotificationService } from './rfq-notification-service.js';
|
|
5
|
+
export interface OrderCompletionReactorDeps {
|
|
6
|
+
emFactory: () => EntityManager;
|
|
7
|
+
/**
|
|
8
|
+
* Feature 075, Phase C — the order row the event names, read over `orders`'
|
|
9
|
+
* published port instead of `em.findOne(Order, …)` against its table.
|
|
10
|
+
* Deactivation drops no tables, so the reactor kept completing quote requests
|
|
11
|
+
* from a module an operator had switched off; over the port the read fails
|
|
12
|
+
* closed, and `orders` is a binding dependency of this manifest.
|
|
13
|
+
*/
|
|
14
|
+
orders: OrderReadPort;
|
|
15
|
+
eventService: RfqEventService;
|
|
16
|
+
notificationService: RfqNotificationService;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* US5 — when an order is created carrying `sourceQuoteRequestId`, flip the
|
|
20
|
+
* originating RFQ to Completed and notify both parties (FR-007 + FR-028).
|
|
21
|
+
*
|
|
22
|
+
* The order-creation flow lives in `orders`, so this observes `order.created.v1`
|
|
23
|
+
* rather than coupling the two modules. The **subscription** lives in this
|
|
24
|
+
* module's `backend.ts` and goes through `ctx.subscribe` (issue #107): it was a
|
|
25
|
+
* bare `eventBus.on` in the plugin body, which meant a switched-off
|
|
26
|
+
* `quote_requests` still completed quote requests, appended an event row and
|
|
27
|
+
* enqueued a customer notification.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createOrderCompletionReactor(deps: OrderCompletionReactorDeps): {
|
|
30
|
+
onOrderCreated: (payload: unknown) => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=order-completion-reactor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-completion-reactor.d.ts","sourceRoot":"","sources":["../../../src/backend/services/order-completion-reactor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAE5E,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B;;;;;;OAMG;IACH,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,EAAE,eAAe,CAAC;IAC9B,mBAAmB,EAAE,sBAAsB,CAAC;CAC7C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,0BAA0B,GAAG;IAC9E,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD,CAyCA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { QuoteRequest } from '../entities/quote-request.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* US5 — when an order is created carrying `sourceQuoteRequestId`, flip the
|
|
4
|
+
* originating RFQ to Completed and notify both parties (FR-007 + FR-028).
|
|
5
|
+
*
|
|
6
|
+
* The order-creation flow lives in `orders`, so this observes `order.created.v1`
|
|
7
|
+
* rather than coupling the two modules. The **subscription** lives in this
|
|
8
|
+
* module's `backend.ts` and goes through `ctx.subscribe` (issue #107): it was a
|
|
9
|
+
* bare `eventBus.on` in the plugin body, which meant a switched-off
|
|
10
|
+
* `quote_requests` still completed quote requests, appended an event row and
|
|
11
|
+
* enqueued a customer notification.
|
|
12
|
+
*/
|
|
13
|
+
export function createOrderCompletionReactor(deps) {
|
|
14
|
+
return {
|
|
15
|
+
async onOrderCreated(payload) {
|
|
16
|
+
// command-coverage-ignore: **this marker is new; the write is not.** It
|
|
17
|
+
// appears only because issue #107 lifted this handler out of a plugin-body
|
|
18
|
+
// closure, where `check-command-coverage` could not see it — the same
|
|
19
|
+
// completion has been written on every quote-to-order conversion since
|
|
20
|
+
// US5. Read it as a write becoming visible, not as a new unaudited one.
|
|
21
|
+
//
|
|
22
|
+
// The disposition: a system reaction to an already-committed order, not an
|
|
23
|
+
// operator-initiated write. The placement that emits the event runs
|
|
24
|
+
// through the Command Bus, and the completion is appended to the RFQ's own
|
|
25
|
+
// event timeline below with `actor: System`, which is this module's audit
|
|
26
|
+
// trail — the same reasoning as its four sibling markers
|
|
27
|
+
// (`rfq-expiry-worker`, `rfq-event-service`, `rfq-revision-service`,
|
|
28
|
+
// `rfq-notification-service`).
|
|
29
|
+
const em = deps.emFactory();
|
|
30
|
+
const orderId = payload.orderId;
|
|
31
|
+
const order = await deps.orders.findById(orderId);
|
|
32
|
+
if (!order || !order.sourceQuoteRequestId)
|
|
33
|
+
return;
|
|
34
|
+
const rfq = await em.findOne(QuoteRequest, { id: order.sourceQuoteRequestId });
|
|
35
|
+
if (!rfq || rfq.status === 'Completed')
|
|
36
|
+
return;
|
|
37
|
+
rfq.status = 'Completed';
|
|
38
|
+
rfq.completedAt = new Date();
|
|
39
|
+
rfq.convertedOrderId = order.id;
|
|
40
|
+
rfq.version += 1;
|
|
41
|
+
await em.flush();
|
|
42
|
+
const evt = await deps.eventService.append({
|
|
43
|
+
quoteRequestId: rfq.id,
|
|
44
|
+
eventType: 'completed',
|
|
45
|
+
actor: { roleLabel: 'System' },
|
|
46
|
+
payload: { type: 'completed', orderId: order.id },
|
|
47
|
+
});
|
|
48
|
+
await deps.notificationService.enqueue({
|
|
49
|
+
quoteRequestId: rfq.id,
|
|
50
|
+
sourceEventId: evt.id,
|
|
51
|
+
recipients: [{ customerAccountId: rfq.customerAccountId }],
|
|
52
|
+
channels: ['email', 'in_app'],
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=order-completion-reactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-completion-reactor.js","sourceRoot":"","sources":["../../../src/backend/services/order-completion-reactor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAkBnE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAgC;IAG3E,OAAO;QACL,KAAK,CAAC,cAAc,CAAC,OAAgB;YACnC,wEAAwE;YACxE,2EAA2E;YAC3E,sEAAsE;YACtE,uEAAuE;YACvE,wEAAwE;YACxE,EAAE;YACF,2EAA2E;YAC3E,oEAAoE;YACpE,2EAA2E;YAC3E,0EAA0E;YAC1E,yDAAyD;YACzD,qEAAqE;YACrE,+BAA+B;YAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAI,OAA+B,CAAC,OAAO,CAAC;YACzD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClD,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,oBAAoB;gBAAE,OAAO;YAClD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,oBAAoB,EAAE,CAAC,CAAC;YAC/E,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,WAAW;gBAAE,OAAO;YAC/C,GAAG,CAAC,MAAM,GAAG,WAAW,CAAC;YACzB,GAAG,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;YAC7B,GAAG,CAAC,gBAAgB,GAAG,KAAK,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;YACjB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;gBACzC,cAAc,EAAE,GAAG,CAAC,EAAE;gBACtB,SAAS,EAAE,WAAW;gBACtB,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;gBAC9B,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE;aAClD,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC;gBACrC,cAAc,EAAE,GAAG,CAAC,EAAE;gBACtB,aAAa,EAAE,GAAG,CAAC,EAAE;gBACrB,UAAU,EAAE,CAAC,EAAE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,CAAC;gBAC1D,QAAQ,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC;aAC9B,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the admin-configurable business-ID prefix/suffix for Quote
|
|
4
|
+
* Requests. In production these wrap
|
|
5
|
+
* `SettingsService.get('quote_requests.business_id.*')` (wired in
|
|
6
|
+
* composition); the generator treats any resolver failure as an empty string
|
|
7
|
+
* so RFQ creation never fails because a setting is missing. Unlike Orders,
|
|
8
|
+
* Quote Requests are not Sales-Channel scoped, so the resolver takes no
|
|
9
|
+
* arguments.
|
|
10
|
+
*/
|
|
11
|
+
export interface QuoteRequestBusinessIdSettingsResolver {
|
|
12
|
+
resolvePrefix(): Promise<string>;
|
|
13
|
+
resolveSuffix(): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
/** Draws the next value from the monotonic business-ID sequence. */
|
|
16
|
+
export type DrawSequence = (em: EntityManager) => Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Generates the customer-facing business Quote Request ID:
|
|
19
|
+
* `${prefix}${sequence}${suffix}`. The numeric core comes from the
|
|
20
|
+
* `quote_requests_business_id_seq` Postgres sequence (monotonic, never
|
|
21
|
+
* resets), so the ID is unique regardless of later prefix/suffix changes. The
|
|
22
|
+
* sequence draw is injected so the generator is unit-testable without a
|
|
23
|
+
* database. Mirrors `orders/services/business-id-generator.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export declare class QuoteRequestBusinessIdGenerator {
|
|
26
|
+
private readonly drawSequence;
|
|
27
|
+
private readonly settings?;
|
|
28
|
+
constructor(drawSequence: DrawSequence, settings?: QuoteRequestBusinessIdSettingsResolver | undefined);
|
|
29
|
+
generate(em: EntityManager): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a prefix/suffix, swallowing any resolver failure as `''`.
|
|
32
|
+
*
|
|
33
|
+
* The catch-all is deliberate and stays (feature 072, D-43): `drawSequence`
|
|
34
|
+
* runs before it, so a throw rolls the transaction back after `nextval` has
|
|
35
|
+
* been consumed — the sequence gaps and the RFQ is refused over a cosmetic
|
|
36
|
+
* prefix. What changes is that it is observable, and that a disabled module
|
|
37
|
+
* still refuses rather than being turned into a missing affix.
|
|
38
|
+
*/
|
|
39
|
+
private resolve;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Production factory: draws `nextval('quote_requests_business_id_seq')` on the
|
|
43
|
+
* EntityManager's connection.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createQuoteRequestBusinessIdGenerator(settings?: QuoteRequestBusinessIdSettingsResolver): QuoteRequestBusinessIdGenerator;
|
|
46
|
+
//# sourceMappingURL=quote-request-business-id-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-request-business-id-generator.d.ts","sourceRoot":"","sources":["../../../src/backend/services/quote-request-business-id-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAS3D;;;;;;;;GAQG;AACH,MAAM,WAAW,sCAAsC;IACrD,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,oEAAoE;AACpE,MAAM,MAAM,YAAY,GAAG,CAAC,EAAE,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,qBAAa,+BAA+B;IAExC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBADT,YAAY,EAAE,YAAY,EAC1B,QAAQ,CAAC,EAAE,sCAAsC,YAAA;IAG9D,QAAQ,CAAC,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IAOlD;;;;;;;;OAQG;YACW,OAAO;CAqBtB;AAED;;;GAGG;AACH,wBAAgB,qCAAqC,CACnD,QAAQ,CAAC,EAAE,sCAAsC,GAChD,+BAA+B,CAQjC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ModuleDisabledError } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* Conditions already reported. Per process and never reset, so a settings
|
|
4
|
+
* outage costs one line rather than one per quote request.
|
|
5
|
+
*/
|
|
6
|
+
const warnedConditions = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Generates the customer-facing business Quote Request ID:
|
|
9
|
+
* `${prefix}${sequence}${suffix}`. The numeric core comes from the
|
|
10
|
+
* `quote_requests_business_id_seq` Postgres sequence (monotonic, never
|
|
11
|
+
* resets), so the ID is unique regardless of later prefix/suffix changes. The
|
|
12
|
+
* sequence draw is injected so the generator is unit-testable without a
|
|
13
|
+
* database. Mirrors `orders/services/business-id-generator.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export class QuoteRequestBusinessIdGenerator {
|
|
16
|
+
drawSequence;
|
|
17
|
+
settings;
|
|
18
|
+
constructor(drawSequence, settings) {
|
|
19
|
+
this.drawSequence = drawSequence;
|
|
20
|
+
this.settings = settings;
|
|
21
|
+
}
|
|
22
|
+
async generate(em) {
|
|
23
|
+
const sequence = await this.drawSequence(em);
|
|
24
|
+
const prefix = await this.resolve((s) => s.resolvePrefix());
|
|
25
|
+
const suffix = await this.resolve((s) => s.resolveSuffix());
|
|
26
|
+
return `${prefix}${sequence}${suffix}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a prefix/suffix, swallowing any resolver failure as `''`.
|
|
30
|
+
*
|
|
31
|
+
* The catch-all is deliberate and stays (feature 072, D-43): `drawSequence`
|
|
32
|
+
* runs before it, so a throw rolls the transaction back after `nextval` has
|
|
33
|
+
* been consumed — the sequence gaps and the RFQ is refused over a cosmetic
|
|
34
|
+
* prefix. What changes is that it is observable, and that a disabled module
|
|
35
|
+
* still refuses rather than being turned into a missing affix.
|
|
36
|
+
*/
|
|
37
|
+
async resolve(pick) {
|
|
38
|
+
if (!this.settings)
|
|
39
|
+
return '';
|
|
40
|
+
try {
|
|
41
|
+
return (await pick(this.settings)) ?? '';
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof ModuleDisabledError)
|
|
45
|
+
throw error;
|
|
46
|
+
const name = error.name ?? 'Error';
|
|
47
|
+
const condition = `rfq-business-id:${name}`;
|
|
48
|
+
if (!warnedConditions.has(condition)) {
|
|
49
|
+
warnedConditions.add(condition);
|
|
50
|
+
console.warn(`[quote_requests] business-ID affix settings unreadable (${name}) — quote ` +
|
|
51
|
+
`requests are numbered without the configured prefix/suffix ` +
|
|
52
|
+
`(logged once per process).`);
|
|
53
|
+
}
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Production factory: draws `nextval('quote_requests_business_id_seq')` on the
|
|
60
|
+
* EntityManager's connection.
|
|
61
|
+
*/
|
|
62
|
+
export function createQuoteRequestBusinessIdGenerator(settings) {
|
|
63
|
+
const draw = async (em) => {
|
|
64
|
+
const rows = (await em.execute(`select nextval('quote_requests_business_id_seq') as n`));
|
|
65
|
+
return String(rows[0]?.n ?? '');
|
|
66
|
+
};
|
|
67
|
+
return new QuoteRequestBusinessIdGenerator(draw, settings);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=quote-request-business-id-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-request-business-id-generator.js","sourceRoot":"","sources":["../../../src/backend/services/quote-request-business-id-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAEvE;;;GAGG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAmB3C;;;;;;;GAOG;AACH,MAAM,OAAO,+BAA+B;IAEvB;IACA;IAFnB,YACmB,YAA0B,EAC1B,QAAiD;QADjD,iBAAY,GAAZ,YAAY,CAAc;QAC1B,aAAQ,GAAR,QAAQ,CAAyC;IACjE,CAAC;IAEJ,KAAK,CAAC,QAAQ,CAAC,EAAiB;QAC9B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;QAC5D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;QAC5D,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;IACzC,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,OAAO,CACnB,IAAoE;QAEpE,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,mBAAmB;gBAAE,MAAM,KAAK,CAAC;YACtD,MAAM,IAAI,GAAI,KAA2B,CAAC,IAAI,IAAI,OAAO,CAAC;YAC1D,MAAM,SAAS,GAAG,mBAAmB,IAAI,EAAE,CAAC;YAC5C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAChC,OAAO,CAAC,IAAI,CACV,2DAA2D,IAAI,YAAY;oBACzE,6DAA6D;oBAC7D,4BAA4B,CAC/B,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,qCAAqC,CACnD,QAAiD;IAEjD,MAAM,IAAI,GAAiB,KAAK,EAAE,EAAE,EAAE,EAAE;QACtC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAC5B,uDAAuD,CACxD,CAAkC,CAAC;QACpC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC,CAAC;IACF,OAAO,IAAI,+BAA+B,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { QuoteRequestLineRecord, QuoteRequestReadPort, QuoteRequestRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import { QuoteRequest } from '../entities/quote-request.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* The row-level read model `quote_requests` publishes (feature 075, Phase P).
|
|
6
|
+
*
|
|
7
|
+
* Two consumers, two questions. `carts` resolves the quote a cart was
|
|
8
|
+
* converted from; `organizations` counts the open quotes per organisation on
|
|
9
|
+
* the sales-rep screen, where the three open statuses had been written out as
|
|
10
|
+
* a literal array in a module that does not own the lifecycle.
|
|
11
|
+
*
|
|
12
|
+
* The full customer-facing projection stays `RfqService` — this is the row,
|
|
13
|
+
* for the callers that only need to count or cross-reference one.
|
|
14
|
+
*/
|
|
15
|
+
export declare class QuoteRequestReadService implements QuoteRequestReadPort {
|
|
16
|
+
private readonly emFactory;
|
|
17
|
+
constructor(emFactory: () => EntityManager);
|
|
18
|
+
findById(id: string): Promise<QuoteRequestRecord | null>;
|
|
19
|
+
/**
|
|
20
|
+
* The business id is unique by construction (`QR-<uuid>` unless a caller
|
|
21
|
+
* passes one) and the column carries a unique index, so `findOne` is the
|
|
22
|
+
* whole of the lookup.
|
|
23
|
+
*/
|
|
24
|
+
findByBusinessId(businessId: string): Promise<QuoteRequestRecord | null>;
|
|
25
|
+
listOpenForOrganizations(organizationIds: readonly string[]): Promise<QuoteRequestRecord[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Added in feature 075's `carts` cut. The quote-to-cart conversion read
|
|
28
|
+
* `QuoteRequestItem` itself; the money columns are deliberately not on the
|
|
29
|
+
* record, because that conversion re-resolves every price against the
|
|
30
|
+
* buyer's current list (FR-017) and a caller that cannot see the quoted
|
|
31
|
+
* price cannot carry it over by accident.
|
|
32
|
+
*/
|
|
33
|
+
listItems(quoteRequestId: string): Promise<QuoteRequestLineRecord[]>;
|
|
34
|
+
}
|
|
35
|
+
export declare function toQuoteRequestRecord(quote: QuoteRequest): QuoteRequestRecord;
|
|
36
|
+
//# sourceMappingURL=quote-request-read-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-request-read-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/quote-request-read-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAGnE;;;;;;;;;;GAUG;AACH,qBAAa,uBAAwB,YAAW,oBAAoB;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAK9D;;;;OAIG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAKxE,wBAAwB,CAC5B,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAYhC;;;;;;OAMG;IACG,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;CAoB3E;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,kBAAkB,CA0B5E"}
|