@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,70 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { QuoteRequest } from '../entities/quote-request.entity.js';
|
|
3
|
+
import { withSystemScope } from '@endora-commerce/platform/tenancy';
|
|
4
|
+
export class RfqExpiryWorker {
|
|
5
|
+
deps;
|
|
6
|
+
constructor(deps) {
|
|
7
|
+
this.deps = deps;
|
|
8
|
+
}
|
|
9
|
+
async sweep(now = new Date()) {
|
|
10
|
+
// command-coverage-ignore: background expiry sweep — flips overdue RFQs to
|
|
11
|
+
// Expired on a timer, a system lifecycle job, not an operator-initiated write.
|
|
12
|
+
// Feature 050 — system-wide sweep across all orgs; run under a system scope
|
|
13
|
+
// so the QuoteRequest reads/writes carry a tenant context (fail-closed guard).
|
|
14
|
+
return withSystemScope('rfq-expiry sweep', async () => {
|
|
15
|
+
const expiryDays = await this.deps.resolveExpiryDays();
|
|
16
|
+
if (!Number.isFinite(expiryDays) || expiryDays <= 0) {
|
|
17
|
+
return { expiredCount: 0 };
|
|
18
|
+
}
|
|
19
|
+
const em = this.deps.emFactory();
|
|
20
|
+
const cutoff = new Date(now.getTime() - expiryDays * 86_400_000);
|
|
21
|
+
const expirable = await em.find(QuoteRequest, {
|
|
22
|
+
status: { $in: ['Pending', 'Created from admin'] },
|
|
23
|
+
updatedAt: { $lt: cutoff },
|
|
24
|
+
});
|
|
25
|
+
if (expirable.length === 0)
|
|
26
|
+
return { expiredCount: 0 };
|
|
27
|
+
for (const rfq of expirable) {
|
|
28
|
+
rfq.status = 'Expired';
|
|
29
|
+
rfq.expiredAt = now;
|
|
30
|
+
rfq.version += 1;
|
|
31
|
+
}
|
|
32
|
+
await em.flush();
|
|
33
|
+
for (const rfq of expirable) {
|
|
34
|
+
const evt = await this.deps.eventService.append({
|
|
35
|
+
quoteRequestId: rfq.id,
|
|
36
|
+
eventType: 'expired',
|
|
37
|
+
actor: { roleLabel: 'System' },
|
|
38
|
+
payload: { type: 'expired', expiryDaysAtTime: expiryDays },
|
|
39
|
+
});
|
|
40
|
+
const recipients = [
|
|
41
|
+
{ customerAccountId: rfq.customerAccountId },
|
|
42
|
+
];
|
|
43
|
+
const assignments = await this.deps.salesRepAssignment.listForOrganization(rfq.organizationId);
|
|
44
|
+
if (assignments.length > 0) {
|
|
45
|
+
for (const a of assignments)
|
|
46
|
+
recipients.push({ adminUserId: a.adminUserId });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Unassigned-org fallback — notify every active admin.
|
|
50
|
+
const everyAdmin = await this.deps.adminUsers.listAll();
|
|
51
|
+
for (const a of everyAdmin)
|
|
52
|
+
recipients.push({ adminUserId: a.id });
|
|
53
|
+
}
|
|
54
|
+
await this.deps.notificationService.enqueue({
|
|
55
|
+
quoteRequestId: rfq.id,
|
|
56
|
+
sourceEventId: evt.id,
|
|
57
|
+
recipients,
|
|
58
|
+
channels: ['email', 'in_app'],
|
|
59
|
+
});
|
|
60
|
+
this.deps.events.emit('rfq.expired.v1', {
|
|
61
|
+
eventId: randomUUID(),
|
|
62
|
+
occurredAt: now.toISOString(),
|
|
63
|
+
rfqId: rfq.id,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { expiredCount: expirable.length };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=rfq-expiry-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-expiry-worker.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-expiry-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type QuoteRequestNotificationChannel } from '../entities/quote-request-notification-event.entity.js';
|
|
3
|
+
/**
|
|
4
|
+
* Notification fan-out for Quote Request lifecycle transitions.
|
|
5
|
+
*
|
|
6
|
+
* The unique index on
|
|
7
|
+
* (quote_request_id, source_event_id, recipient*, channel) is enforced
|
|
8
|
+
* at the DB level (migration 029); duplicate-row attempts are caught
|
|
9
|
+
* here as Postgres `23505` and silently ignored, keeping retries
|
|
10
|
+
* idempotent (FR-030 + research §R4).
|
|
11
|
+
*
|
|
12
|
+
* Actual transport (email send, in-app surface push) is handled by the
|
|
13
|
+
* foundation `notifications` BullMQ queue — out of scope for this
|
|
14
|
+
* service. We only persist the row and mark it `queued`; the queue
|
|
15
|
+
* worker flips it to `sent` / `failed`.
|
|
16
|
+
*/
|
|
17
|
+
export interface NotificationRecipient {
|
|
18
|
+
adminUserId?: string | null;
|
|
19
|
+
customerAccountId?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface EnqueueNotificationInput {
|
|
22
|
+
quoteRequestId: string;
|
|
23
|
+
sourceEventId: string;
|
|
24
|
+
recipients: NotificationRecipient[];
|
|
25
|
+
channels: QuoteRequestNotificationChannel[];
|
|
26
|
+
}
|
|
27
|
+
export declare class RfqNotificationService {
|
|
28
|
+
private readonly emFactory;
|
|
29
|
+
constructor(emFactory: () => EntityManager);
|
|
30
|
+
/**
|
|
31
|
+
* Persist one row per recipient × channel. Returns the number of
|
|
32
|
+
* rows actually inserted (after dedupe).
|
|
33
|
+
*/
|
|
34
|
+
enqueue(input: EnqueueNotificationInput): Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=rfq-notification-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-notification-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-notification-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,wDAAwD,CAAC;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,qBAAqB,EAAE,CAAC;IACpC,QAAQ,EAAE,+BAA+B,EAAE,CAAC;CAC7C;AAED,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAE3D;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC;CAgChE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { QuoteRequestNotificationEvent, } from '../entities/quote-request-notification-event.entity.js';
|
|
2
|
+
export class RfqNotificationService {
|
|
3
|
+
emFactory;
|
|
4
|
+
constructor(emFactory) {
|
|
5
|
+
this.emFactory = emFactory;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Persist one row per recipient × channel. Returns the number of
|
|
9
|
+
* rows actually inserted (after dedupe).
|
|
10
|
+
*/
|
|
11
|
+
async enqueue(input) {
|
|
12
|
+
// command-coverage-ignore: notification delivery queue — writes per-recipient
|
|
13
|
+
// dispatch rows, delivery bookkeeping, not an audited domain-state mutation.
|
|
14
|
+
const em = this.emFactory();
|
|
15
|
+
let inserted = 0;
|
|
16
|
+
for (const recipient of input.recipients) {
|
|
17
|
+
// Skip empty recipient (would violate the actor-XOR invariant if both
|
|
18
|
+
// adminUserId and customerAccountId are null).
|
|
19
|
+
if (!recipient.adminUserId && !recipient.customerAccountId)
|
|
20
|
+
continue;
|
|
21
|
+
for (const channel of input.channels) {
|
|
22
|
+
const event = em.create(QuoteRequestNotificationEvent, {
|
|
23
|
+
quoteRequestId: input.quoteRequestId,
|
|
24
|
+
sourceEventId: input.sourceEventId,
|
|
25
|
+
recipientAdminUserId: recipient.adminUserId ?? null,
|
|
26
|
+
recipientCustomerAccountId: recipient.customerAccountId ?? null,
|
|
27
|
+
channel,
|
|
28
|
+
status: 'queued',
|
|
29
|
+
});
|
|
30
|
+
try {
|
|
31
|
+
await em.persistAndFlush(event);
|
|
32
|
+
inserted += 1;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// Postgres unique-violation on (rfq, event, recipient, channel) —
|
|
36
|
+
// this is the dedupe guarantee firing on a retry.
|
|
37
|
+
em.clear();
|
|
38
|
+
if (!isUniqueViolation(err))
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return inserted;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isUniqueViolation(err) {
|
|
47
|
+
if (typeof err !== 'object' || err === null)
|
|
48
|
+
return false;
|
|
49
|
+
const candidate = err;
|
|
50
|
+
return candidate.code === '23505';
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=rfq-notification-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-notification-service.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-notification-service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wDAAwD,CAAC;AA4BhE,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,KAA+B;QAC3C,8EAA8E;QAC9E,6EAA6E;QAC7E,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACzC,sEAAsE;YACtE,+CAA+C;YAC/C,IAAI,CAAC,SAAS,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,iBAAiB;gBAAE,SAAS;YAErE,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACrC,MAAM,KAAK,GAAG,EAAE,CAAC,MAAM,CAAC,6BAA6B,EAAE;oBACrD,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,aAAa,EAAE,KAAK,CAAC,aAAa;oBAClC,oBAAoB,EAAE,SAAS,CAAC,WAAW,IAAI,IAAI;oBACnD,0BAA0B,EAAE,SAAS,CAAC,iBAAiB,IAAI,IAAI;oBAC/D,OAAO;oBACP,MAAM,EAAE,QAAQ;iBACjB,CAAC,CAAC;gBACH,IAAI,CAAC;oBACH,MAAM,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;oBAChC,QAAQ,IAAI,CAAC,CAAC;gBAChB,CAAC;gBAAC,OAAO,GAAY,EAAE,CAAC;oBACtB,kEAAkE;oBAClE,kDAAkD;oBAClD,EAAE,CAAC,KAAK,EAAE,CAAC;oBACX,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC;wBAAE,MAAM,GAAG,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF;AAED,SAAS,iBAAiB,CAAC,GAAY;IACrC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,SAAS,GAAG,GAAyB,CAAC;IAC5C,OAAO,SAAS,CAAC,IAAI,KAAK,OAAO,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { QuoteRequestRevision, type QuoteRequestRevisionLine } from '../entities/quote-request-revision.entity.js';
|
|
3
|
+
import type { QuoteRequest } from '../entities/quote-request.entity.js';
|
|
4
|
+
import type { QuoteRequestItem } from '../entities/quote-request-item.entity.js';
|
|
5
|
+
/**
|
|
6
|
+
* Coarse-grained revision snapshot writer + diff calculator. Each
|
|
7
|
+
* `modify` event in the lifecycle creates one row in
|
|
8
|
+
* quote_request_revisions; the customer-facing comparison view
|
|
9
|
+
* (`comparisonAgainstLastSeen`) is computed on the fly by
|
|
10
|
+
* `diffRevisions` rather than being persisted.
|
|
11
|
+
*/
|
|
12
|
+
export type RfqDiffEntry = {
|
|
13
|
+
kind: 'header_note';
|
|
14
|
+
before: string | null;
|
|
15
|
+
after: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'line_added';
|
|
18
|
+
productId: string;
|
|
19
|
+
productName: string;
|
|
20
|
+
quantity: number;
|
|
21
|
+
agreedUnitPrice: number | null;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'line_removed';
|
|
24
|
+
productId: string;
|
|
25
|
+
productName: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'line_quantity';
|
|
28
|
+
productId: string;
|
|
29
|
+
productName: string;
|
|
30
|
+
before: number;
|
|
31
|
+
after: number;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'line_agreed_unit_price';
|
|
34
|
+
productId: string;
|
|
35
|
+
productName: string;
|
|
36
|
+
before: number | null;
|
|
37
|
+
after: number | null;
|
|
38
|
+
};
|
|
39
|
+
export interface RecordRevisionInput {
|
|
40
|
+
rfq: QuoteRequest;
|
|
41
|
+
items: QuoteRequestItem[];
|
|
42
|
+
actor: {
|
|
43
|
+
adminUserId?: string | null;
|
|
44
|
+
customerAccountId?: string | null;
|
|
45
|
+
};
|
|
46
|
+
previousRevisionId?: string | null;
|
|
47
|
+
}
|
|
48
|
+
export declare class RfqRevisionService {
|
|
49
|
+
private readonly emFactory;
|
|
50
|
+
constructor(emFactory: () => EntityManager);
|
|
51
|
+
record(input: RecordRevisionInput): Promise<QuoteRequestRevision>;
|
|
52
|
+
/** Returns the latest revision for an RFQ, if any. */
|
|
53
|
+
latestForRfq(quoteRequestId: string): Promise<QuoteRequestRevision | null>;
|
|
54
|
+
/** Returns the revision whose `revisionNumber === n`, if any. */
|
|
55
|
+
byNumber(quoteRequestId: string, n: number): Promise<QuoteRequestRevision | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Pure diff calculator. Takes two snapshots (`before`, `after`) plus
|
|
58
|
+
* their header notes and returns the list of diff entries in the
|
|
59
|
+
* order header → line_added → line_removed → line_quantity →
|
|
60
|
+
* line_agreed_unit_price. Stable by productId so renderers can
|
|
61
|
+
* group/sort easily.
|
|
62
|
+
*/
|
|
63
|
+
diffRevisions(args: {
|
|
64
|
+
beforeHeaderNote: string | null;
|
|
65
|
+
afterHeaderNote: string | null;
|
|
66
|
+
beforeItems: QuoteRequestRevisionLine[];
|
|
67
|
+
afterItems: QuoteRequestRevisionLine[];
|
|
68
|
+
}): RfqDiffEntry[];
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=rfq-revision-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-revision-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-revision-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,oBAAoB,EACpB,KAAK,wBAAwB,EAC9B,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACpE;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,GACD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAChE;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,IAAI,EAAE,wBAAwB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,YAAY,CAAC;IAClB,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,KAAK,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC1E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAsBvE,sDAAsD;IAChD,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAUhF,iEAAiE;IAC3D,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAUvF;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE;QAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,WAAW,EAAE,wBAAwB,EAAE,CAAC;QACxC,UAAU,EAAE,wBAAwB,EAAE,CAAC;KACxC,GAAG,YAAY,EAAE;CA+DnB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { QuoteRequestRevision, } from '../entities/quote-request-revision.entity.js';
|
|
2
|
+
export class RfqRevisionService {
|
|
3
|
+
emFactory;
|
|
4
|
+
constructor(emFactory) {
|
|
5
|
+
this.emFactory = emFactory;
|
|
6
|
+
}
|
|
7
|
+
async record(input) {
|
|
8
|
+
// command-coverage-ignore: immutable per-revision snapshot of the RFQ state
|
|
9
|
+
// (version history), written as part of the audited parent RFQ action.
|
|
10
|
+
const em = this.emFactory();
|
|
11
|
+
const itemsSnapshot = input.items.map(toSnapshotLine);
|
|
12
|
+
// Caller sets `rfq.currentRevisionNumber` to the number of the
|
|
13
|
+
// revision being recorded BEFORE calling record(). This service
|
|
14
|
+
// never auto-bumps so the number on the rfq row and on the
|
|
15
|
+
// revision row are guaranteed to match.
|
|
16
|
+
const rev = em.create(QuoteRequestRevision, {
|
|
17
|
+
quoteRequestId: input.rfq.id,
|
|
18
|
+
revisionNumber: input.rfq.currentRevisionNumber,
|
|
19
|
+
createdByAdminUserId: input.actor.adminUserId ?? null,
|
|
20
|
+
createdByCustomerAccountId: input.actor.customerAccountId ?? null,
|
|
21
|
+
headerNoteSnapshot: input.rfq.headerNote ?? null,
|
|
22
|
+
itemsSnapshot,
|
|
23
|
+
previousRevisionId: input.previousRevisionId ?? null,
|
|
24
|
+
});
|
|
25
|
+
await em.persistAndFlush(rev);
|
|
26
|
+
return rev;
|
|
27
|
+
}
|
|
28
|
+
/** Returns the latest revision for an RFQ, if any. */
|
|
29
|
+
async latestForRfq(quoteRequestId) {
|
|
30
|
+
const em = this.emFactory();
|
|
31
|
+
const found = await em.find(QuoteRequestRevision, { quoteRequestId }, { orderBy: { revisionNumber: 'desc' }, limit: 1 });
|
|
32
|
+
return found[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
/** Returns the revision whose `revisionNumber === n`, if any. */
|
|
35
|
+
async byNumber(quoteRequestId, n) {
|
|
36
|
+
const em = this.emFactory();
|
|
37
|
+
const found = await em.find(QuoteRequestRevision, { quoteRequestId, revisionNumber: n }, { limit: 1 });
|
|
38
|
+
return found[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pure diff calculator. Takes two snapshots (`before`, `after`) plus
|
|
42
|
+
* their header notes and returns the list of diff entries in the
|
|
43
|
+
* order header → line_added → line_removed → line_quantity →
|
|
44
|
+
* line_agreed_unit_price. Stable by productId so renderers can
|
|
45
|
+
* group/sort easily.
|
|
46
|
+
*/
|
|
47
|
+
diffRevisions(args) {
|
|
48
|
+
const out = [];
|
|
49
|
+
if ((args.beforeHeaderNote ?? null) !== (args.afterHeaderNote ?? null)) {
|
|
50
|
+
out.push({
|
|
51
|
+
kind: 'header_note',
|
|
52
|
+
before: args.beforeHeaderNote ?? null,
|
|
53
|
+
after: args.afterHeaderNote ?? null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const beforeByProduct = new Map(args.beforeItems.map((l) => [l.productId, l]));
|
|
57
|
+
const afterByProduct = new Map(args.afterItems.map((l) => [l.productId, l]));
|
|
58
|
+
for (const [productId, after] of afterByProduct) {
|
|
59
|
+
if (!beforeByProduct.has(productId)) {
|
|
60
|
+
out.push({
|
|
61
|
+
kind: 'line_added',
|
|
62
|
+
productId,
|
|
63
|
+
productName: after.productName,
|
|
64
|
+
quantity: after.quantity,
|
|
65
|
+
agreedUnitPrice: after.agreedUnitPrice,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [productId, before] of beforeByProduct) {
|
|
70
|
+
if (!afterByProduct.has(productId)) {
|
|
71
|
+
out.push({
|
|
72
|
+
kind: 'line_removed',
|
|
73
|
+
productId,
|
|
74
|
+
productName: before.productName,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const [productId, after] of afterByProduct) {
|
|
79
|
+
const before = beforeByProduct.get(productId);
|
|
80
|
+
if (!before)
|
|
81
|
+
continue;
|
|
82
|
+
if (before.quantity !== after.quantity) {
|
|
83
|
+
out.push({
|
|
84
|
+
kind: 'line_quantity',
|
|
85
|
+
productId,
|
|
86
|
+
productName: after.productName,
|
|
87
|
+
before: before.quantity,
|
|
88
|
+
after: after.quantity,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const beforePrice = before.agreedUnitPrice;
|
|
92
|
+
const afterPrice = after.agreedUnitPrice;
|
|
93
|
+
if (beforePrice !== afterPrice) {
|
|
94
|
+
out.push({
|
|
95
|
+
kind: 'line_agreed_unit_price',
|
|
96
|
+
productId,
|
|
97
|
+
productName: after.productName,
|
|
98
|
+
before: beforePrice,
|
|
99
|
+
after: afterPrice,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function toSnapshotLine(item) {
|
|
107
|
+
return {
|
|
108
|
+
productId: item.productId,
|
|
109
|
+
variantId: item.variantId ?? null,
|
|
110
|
+
productName: item.productName,
|
|
111
|
+
productSlug: item.productSlug ?? null,
|
|
112
|
+
quantity: item.quantity,
|
|
113
|
+
desiredUnitPrice: item.desiredUnitPrice != null ? Number(item.desiredUnitPrice) : null,
|
|
114
|
+
agreedUnitPrice: item.agreedUnitPrice != null ? Number(item.agreedUnitPrice) : null,
|
|
115
|
+
lineNote: item.lineNote ?? null,
|
|
116
|
+
lineCurrency: item.lineCurrency,
|
|
117
|
+
discountPercent: item.discountPercent != null ? Number(item.discountPercent) : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=rfq-revision-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-revision-service.js","sourceRoot":"","sources":["../../../src/backend/services/rfq-revision-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type QuoteRequest as RfqDto, type QuoteRequestSummary, type CreateQuoteRequest, type PatchQuoteRequest, type ResubmitQuoteRequest, type AdminUserReadPort, type CartWritePort, type CatalogProductReadPort, type CustomerAccountReadPort, type SalesRepAssignmentPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { EventBase, EventBus } from '@endora-commerce/platform/events';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { QuoteRequest } from '../entities/quote-request.entity.js';
|
|
6
|
+
import type { RfqEventService } from './rfq-event-service.js';
|
|
7
|
+
import type { RfqRevisionService } from './rfq-revision-service.js';
|
|
8
|
+
import type { RfqNotificationService } from './rfq-notification-service.js';
|
|
9
|
+
import type { QuoteRequestBusinessIdGenerator } from './quote-request-business-id-generator.js';
|
|
10
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
11
|
+
/**
|
|
12
|
+
* Customer-facing Quote Requests service — feature 008 workflow.
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle methods exposed (each maps to one HTTP route):
|
|
15
|
+
* - list / getById (US1)
|
|
16
|
+
* - create + submit (US1)
|
|
17
|
+
* - patchDraft (US1, customer-side edit before any internal-side action)
|
|
18
|
+
* - resubmit (US1, "Submit again")
|
|
19
|
+
* - acceptRevision / rejectRevision (US3 — also covers Created from admin per US4)
|
|
20
|
+
* - convertToOrder (US5)
|
|
21
|
+
*
|
|
22
|
+
* The service writes through RfqEventService, RfqRevisionService, and
|
|
23
|
+
* RfqNotificationService for every state change so audit + comparison
|
|
24
|
+
* + notifications are consistent across all entry points.
|
|
25
|
+
*/
|
|
26
|
+
export interface RfqEvents extends Record<string, EventBase> {
|
|
27
|
+
'rfq.created.v1': EventBase & {
|
|
28
|
+
rfqId: string;
|
|
29
|
+
organizationId: string;
|
|
30
|
+
};
|
|
31
|
+
'rfq.approved.v1': EventBase & {
|
|
32
|
+
rfqId: string;
|
|
33
|
+
};
|
|
34
|
+
'rfq.canceled.v1': EventBase & {
|
|
35
|
+
rfqId: string;
|
|
36
|
+
reason: string | null;
|
|
37
|
+
};
|
|
38
|
+
'rfq.modified.v1': EventBase & {
|
|
39
|
+
rfqId: string;
|
|
40
|
+
revisionNumber: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export type RfqEventBus = EventBus<RfqEvents>;
|
|
44
|
+
export interface CustomerContext {
|
|
45
|
+
customerAccountId: string;
|
|
46
|
+
organizationId: string;
|
|
47
|
+
/** True when the customer account holds the org-admin role on the
|
|
48
|
+
* current organization (FR-011 — broader visibility). */
|
|
49
|
+
isOrgAdmin: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface RfqServiceDeps {
|
|
52
|
+
emFactory: () => EntityManager;
|
|
53
|
+
events: RfqEventBus;
|
|
54
|
+
eventService: RfqEventService;
|
|
55
|
+
revisionService: RfqRevisionService;
|
|
56
|
+
notificationService: RfqNotificationService;
|
|
57
|
+
salesRepAssignment: SalesRepAssignmentPort;
|
|
58
|
+
/**
|
|
59
|
+
* Feature 075, Phase C — the four rows this service reads and none of which
|
|
60
|
+
* it owns, plus the one it writes.
|
|
61
|
+
*
|
|
62
|
+
* Every one of them was an `em.find` / `em.create` against another module's
|
|
63
|
+
* table, which is the shape Principle XVII cannot gate: deactivation drops no
|
|
64
|
+
* tables, so a quote kept resolving product names, requester identities and
|
|
65
|
+
* admin recipients — and kept seeding a cart — out of modules an operator had
|
|
66
|
+
* switched off. All four owners are binding `dependencies` of this manifest
|
|
67
|
+
* and every read fails closed; a quote priced against a catalogue the
|
|
68
|
+
* platform is not serving is worse than a refused quote.
|
|
69
|
+
*
|
|
70
|
+
* `carts` is the write, and it is the one that mattered most:
|
|
71
|
+
* `convertToOrder` created a `Cart` and hand-built `CartItem` rows, with the
|
|
72
|
+
* clear-then-seed rule spelled out here and `lastActivityAt` maintained
|
|
73
|
+
* nowhere. `replaceItemsForCustomer` is the port `carts` published for
|
|
74
|
+
* exactly this — its own doc comment names this call site — so the operation
|
|
75
|
+
* is owned end to end by the module that owns the tables (D-78 rule 1).
|
|
76
|
+
*/
|
|
77
|
+
catalogProducts: CatalogProductReadPort;
|
|
78
|
+
/**
|
|
79
|
+
* The sanctioned bridge accessor (Constitution XII), for the assortment gate
|
|
80
|
+
* a quote line owes (issue #259). A kernel registration rather than a
|
|
81
|
+
* module's, so it is not one of the four edges the block above declares.
|
|
82
|
+
*/
|
|
83
|
+
channelMembership: SalesChannelMembershipPort;
|
|
84
|
+
customerAccounts: CustomerAccountReadPort;
|
|
85
|
+
adminUsers: AdminUserReadPort;
|
|
86
|
+
carts: CartWritePort;
|
|
87
|
+
/**
|
|
88
|
+
* Generates the customer-facing business Quote Request ID. Optional so
|
|
89
|
+
* legacy/test compositions that don't wire it fall back to the entity's
|
|
90
|
+
* placeholder default.
|
|
91
|
+
*/
|
|
92
|
+
businessId?: QuoteRequestBusinessIdGenerator;
|
|
93
|
+
/**
|
|
94
|
+
* Resolves the VAT rate (fraction, e.g. `0.23`) applied to the quote's net
|
|
95
|
+
* prices for the given Organization. Mirrors the Orders flow.
|
|
96
|
+
*
|
|
97
|
+
* Required since issue #124 — see `QuoteRequestsModuleOptions.resolveTaxRate`.
|
|
98
|
+
*/
|
|
99
|
+
resolveTaxRate: (organizationId: string) => Promise<number>;
|
|
100
|
+
/** Feature 054 — audits RFQ writes co-transactionally when provided. */
|
|
101
|
+
auditLog?: AuditPort;
|
|
102
|
+
}
|
|
103
|
+
export declare class RfqService {
|
|
104
|
+
#private;
|
|
105
|
+
private readonly deps;
|
|
106
|
+
constructor(deps: RfqServiceDeps);
|
|
107
|
+
/**
|
|
108
|
+
* Draws the next business Quote Request ID, or `undefined` when no generator
|
|
109
|
+
* is wired (so the entity placeholder default applies). Shared with
|
|
110
|
+
* RfqAdminService so admin-created RFQs are numbered identically.
|
|
111
|
+
*/
|
|
112
|
+
generateBusinessId(em: EntityManager): Promise<string | undefined>;
|
|
113
|
+
/**
|
|
114
|
+
* Resolves the VAT rate applied to a quote's net prices for an Organization.
|
|
115
|
+
*
|
|
116
|
+
* No unwired branch (issue #124): the resolver is required, and an absent
|
|
117
|
+
* `taxes` module surfaces through it as `MODULE_DISABLED` rather than as a
|
|
118
|
+
* quote silently priced at 0% VAT.
|
|
119
|
+
*/
|
|
120
|
+
taxRateForOrganization(organizationId: string): Promise<number>;
|
|
121
|
+
listForCustomer(ctx: CustomerContext): Promise<QuoteRequestSummary[]>;
|
|
122
|
+
getForCustomer(rfqId: string, ctx: CustomerContext): Promise<RfqDto>;
|
|
123
|
+
createForCustomer(ctx: CustomerContext, input: CreateQuoteRequest): Promise<RfqDto>;
|
|
124
|
+
patchDraft(rfqId: string, ctx: CustomerContext, body: PatchQuoteRequest, expectedVersion: number | null): Promise<RfqDto>;
|
|
125
|
+
resubmit(rfqId: string, ctx: CustomerContext, body: ResubmitQuoteRequest): Promise<RfqDto>;
|
|
126
|
+
acceptRevision(rfqId: string, ctx: CustomerContext, expectedRevisionNumber: number): Promise<RfqDto>;
|
|
127
|
+
rejectRevision(rfqId: string, ctx: CustomerContext, expectedRevisionNumber: number, reason: string | undefined): Promise<RfqDto>;
|
|
128
|
+
private respondToRevision;
|
|
129
|
+
convertToOrder(rfqId: string, ctx: CustomerContext): Promise<{
|
|
130
|
+
cartId: string;
|
|
131
|
+
checkoutUrl: string;
|
|
132
|
+
}>;
|
|
133
|
+
private findVisibleForCustomer;
|
|
134
|
+
private notifyInternalSide;
|
|
135
|
+
/** Serialise to the customer-facing response shape. Hides internal admin
|
|
136
|
+
* actor identity behind `actorRoleLabel` per FR-037 unless
|
|
137
|
+
* `includeFullActorIdentity = true` (admin path). */
|
|
138
|
+
serializeFull(em: EntityManager, rfq: QuoteRequest, includeFullActorIdentity: boolean): Promise<RfqDto>;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=rfq-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rfq-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/rfq-service.ts"],"names":[],"mappings":"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"}
|