From ba70c0abcf794545cd11852d812073be5b05ade6 Mon Sep 17 00:00:00 2001 From: Gerald Baulig Date: Tue, 2 Sep 2025 16:50:44 +0200 Subject: [PATCH] feat(eventloop): check order completion event loop with fulfillments and invoice --- cfg/config.json | 22 +++++++- src/service.ts | 147 +++++++++++++++++++++++++++++++++++++----------- src/utils.ts | 4 +- src/worker.ts | 49 ++++++++++++---- 4 files changed, 177 insertions(+), 45 deletions(-) diff --git a/cfg/config.json b/cfg/config.json index bbfd6d1..be493eb 100644 --- a/cfg/config.json +++ b/cfg/config.json @@ -164,6 +164,12 @@ "renderResponse": { "messageObject": "io.restorecommerce.rendering.RenderResponseList" }, + "fulfillmentModified": { + "messageObject": "io.restorecommerce.fulfillment.Fulfillment" + }, + "invoiceModified": { + "messageObject": "io.restorecommerce.invoice.Invoice" + }, "sendEmail": { "messageObject": "io.restorecommerce.notification_req.NotificationReq" }, @@ -202,6 +208,18 @@ "withdrawOrders": "handleWithdrawOrders", "cancelOrders": "handleCancelOrders" } + }, + "fulfillment.resource": { + "topic": "io.restorecommerce.fulfillment.resource", + "events": { + "fulfillmentModified": "handleStateUpdate" + } + }, + "invoicing.resource": { + "topic": "io.restorecommerce.invoicing.resource", + "events": { + "invoiceModified": "handleStateUpdate" + } } } } @@ -311,7 +329,9 @@ "health": "grpc-health-v1" }, "urns": { - "instanceType": "urn:restorecommerce:acs:model:order:Order", + "order": "urn:restorecommerce:acs:model:order:Order", + "fulfillment": "urn:restorecommerce:acs:model:fulfillment:Fulfillment", + "invoice": "urn:restorecommerce:acs:model:invoice:Invoice", "shop_fulfillment_create_enabled": "urn:restorecommerce:shop:setting:order:submit:fulfillment:create:enabled", "shop_invoice_create_enabled": "urn:restorecommerce:shop:setting:order:submit:invoice:create:enabled", "shop_invoice_render_enabled": "urn:restorecommerce:shop:setting:order:submit:invoice:render:enabled", diff --git a/src/service.ts b/src/service.ts index 275f700..776789e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -65,6 +65,8 @@ import { FulfillmentListResponse, FulfillmentResponse, Packaging, + Fulfillment, + FulfillmentState, } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/fulfillment.js'; import { FulfillmentProductServiceDefinition, @@ -78,6 +80,7 @@ import { } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/filter.js'; import { Filter_ValueType, + FilterOp_Operator, } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/resource_base.js'; import { StatusListResponse @@ -88,7 +91,9 @@ import { Section, Position, InvoiceServiceDefinition, - InvoiceList + InvoiceList, + Invoice, + PaymentState } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/invoice.js'; import { Subject @@ -325,9 +330,11 @@ export class OrderingService return this.name; } + /* get instance_type() { - return this.urns.instanceType; + return this.urns.order; } + */ constructor( protected readonly orderingTopic: Topic, @@ -901,21 +908,13 @@ export class OrderingService fulfillments ??= await this.fulfillment_service!.read( { filters: [{ - filters: [ - { - field: 'references[*].instance_type', - operation: Filter_Operation.in, - value: this.instance_type, - }, - { - field: 'references[*].instance_id', - operation: Filter_Operation.in, - value: JSON.stringify(order_ids), - type: Filter_ValueType.ARRAY - } - ] + filters: order_ids?.map(id => ({ + field: 'references[*].instance_id', + operation: Filter_Operation.in, + value: id, + })), + operator: FilterOp_Operator.or, }], - limit: order_ids.length, subject, }, context, @@ -930,10 +929,10 @@ export class OrderingService } ); - return fulfillments.reduce( + return fulfillments?.reduce( (a, b) => { b.payload?.references.filter( - r => r.instance_type === this.instance_type + r => r.instance_type === this.urns.order ).forEach( r => { const c = a[r.instance_id]; @@ -1673,7 +1672,9 @@ export class OrderingService r => { r.items?.forEach( fulfillment => { - const id = fulfillment.payload?.references?.[0]?.instance_id ?? fulfillment.status?.id; + const id = fulfillment.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id ?? fulfillment.status?.id; const order = response_map[id]; if (order && fulfillment.status?.code >= 300) { order.status = createStatusCode( @@ -1727,7 +1728,9 @@ export class OrderingService r => { r.items?.forEach( fulfillment => { - const id = fulfillment.payload?.references?.[0]?.instance_id ?? fulfillment.status?.id; + const id = fulfillment.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id ?? fulfillment.status?.id; const order = response_map[id]; if (order && fulfillment.status?.code >= 300) { order.status = createStatusCode( @@ -1797,7 +1800,9 @@ export class OrderingService response.invoices.push(...r.items); r.items.forEach( item => { - const id = item.payload?.references?.[0]?.instance_id; + const id = item.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id; const order = response_map[id]; if (order && item.status?.code >= 300) { order.status = createStatusCode( @@ -1863,7 +1868,9 @@ export class OrderingService response.invoices.push(...r.items); r.items.forEach( item => { - const id = item.payload?.references?.[0]?.instance_id; + const id = item.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id; const order = response_map[id]; if (order && item.status?.code >= 300) { order.status = createStatusCode( @@ -1898,9 +1905,13 @@ export class OrderingService this.logger?.debug('Send invoices on submit...'); const invoices = response.invoices?.filter( item => { - const setting = settings.get(item.payload?.references?.[0]?.instance_id); + const setting = settings.get(item.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id); return item.status?.code < 300 - && item.payload?.references?.[0]?.instance_id + && item.payload?.references?.find( + r => r.instance_type === this.urns.order + )?.instance_id && !setting?.shop_invoice_send_disabled; } ).map( @@ -2113,7 +2124,7 @@ export class OrderingService (a, b) => { a[b.order_id] = { reference: { - instance_type: this.instance_type, + instance_type: this.urns.order, instance_id: b.order_id, } }; @@ -2182,7 +2193,7 @@ export class OrderingService const query: FulfillmentSolutionQuery = { reference: { - instance_type: this.instance_type, + instance_type: this.urns.order, instance_id: order.payload.id, }, recipient: order.payload?.shipping_address, @@ -2301,7 +2312,7 @@ export class OrderingService customer_id: order.payload.customer_id, user_id: order.payload.user_id, references: [{ - instance_type: this.instance_type, + instance_type: this.urns.order, instance_id: item.order_id, }], packaging: { @@ -2750,12 +2761,25 @@ export class OrderingService user_id: master.payload.user_id, customer_id: master.payload.customer_id, shop_id: master.payload.shop_id, - references: invoice.sections.map( - section => ({ - instance_type: this.instance_type, - instance_id: section.order_id, - }) - ), + references: invoice.sections?.flatMap( + section => [ + [{ + instance_type: this.urns.order, + instance_id: section.order_id, + }], + section.selected_fulfillments?.map( + f => ({ + instance_type: this.urns.fulfillment, + instance_id: f.fulfillment_id, + }) + ) ?? fulfillment_map[section.order_id]?.map( + f => ({ + instance_type: this.urns.fulfillment, + instance_id: f.payload?.id, + }) + ), + ] + )?.flat().filter(Boolean), customer_remark: master.payload.customer_remark, customer_order_number: master.payload.customer_order_nr, customer_vat_id: master.payload.customer_vat_id, @@ -3190,4 +3214,61 @@ export class OrderingService ); return status?.operation_status; } + + public async checkCompletion( + msg: Invoice | Fulfillment + ) { + const ids = msg.references.filter( + ref => ref.instance_type === this.urns.order + ).map( + ref => ref.instance_id + ); + + const fulfillments_complete = await this.getFulfillmentMap( + ids, + this.tech_user, + ).then( + fm => Object.values(fm).flat().every( + f => f.payload?.fulfillment_state === FulfillmentState.COMPLETE + ) + ); + + const invoices_payed = await this.invoice_service?.read( + { + filters: [{ + filters: ids?.map(id => ({ + field: 'references[*].instance_id', + operation: Filter_Operation.in, + value: id, + })), + operator: FilterOp_Operator.or, + }], + subject: this.tech_user, + }, + ).then( + response => { + if (response.operation_status?.code < 300) { + return response.items; + } + else { + throw response.operation_status; + } + } + ).then( + invoices => invoices.every( + i => i.payload?.payment_state === PaymentState.PAYED + ) + ); + + if (fulfillments_complete && invoices_payed) { + await this.superUpdate({ + items: ids.map(id => ({ + id, + order_state: OrderState.COMPLETED + })), + subject: this.tech_user, + total_count: ids.length, + }); + } + } } diff --git a/src/utils.ts b/src/utils.ts index 669672b..b9c6fa5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -191,7 +191,9 @@ export const toObjectListMap = (items: T[]): Record any; export type HandlerMap = Record; +export type HandlerError = { + code?: number, + message?: string, + details?: string, + stack?: Error['stack'], +}; + export class Worker { private _cfg?: ServiceConfig; private _offsetStore?: OffsetStore; @@ -133,61 +148,70 @@ export class Worker { handleCreateOrders: (msg: OrderList, context: any, config: any, eventName: string) => { return this.orderingService?.create(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}:`, { error }) + error => this.handleError(eventName, error), ); }, handleUpdateOrders: (msg: OrderList, context: any, config: any, eventName: string) => { return this.orderingService?.update(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleUpsertOrders: (msg: OrderList, context: any, config: any, eventName: string) => { return this.orderingService?.upsert(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleSubmitOrders: (msg: OrderList, context: any, config: any, eventName: string) => { return this.orderingService?.submit(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleFulfillOrders: (msg: FulfillmentRequestList, context: any, config: any, eventName: string) => { return this.orderingService?.createFulfillment(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleWithdrawOrders: (msg: OrderIdList, context: any, config: any, eventName: string) => { return this.orderingService?.withdraw(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleCancelOrders: (msg: OrderIdList, context: any, config: any, eventName: string) => { return this.orderingService?.cancel(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleDeleteOrders: (msg: any, context: any, config: any, eventName: string) => { return this.orderingService?.delete(msg, context).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), ); }, handleRenderResponse: (msg: any, context: any, config: any, eventName: string) => { return this.orderingService?.handleRenderResponse(msg).then( () => this.logger?.info(`Event ${eventName} handled.`), - error => this.logger?.error(`Error while handling event ${eventName}: `, { error }) + error => this.handleError(eventName, error), + ); + }, + handleStateUpdate: (msg: any, context: any, config: any, eventName: string) => { + return this.orderingService?.checkCompletion(msg).then( + () => this.logger?.info(`Event ${eventName} handled.`), + error => this.handleError(eventName, error), ); }, handleQueuedJob: (msg: any, context: any, config: any, eventName: string) => { return this.serviceActions?.get(msg?.type)?.(msg?.data?.payload, context, config, msg?.type).then( () => this.logger?.info(`Job ${msg?.type} done.`), - (error: any) => this.logger?.error(`Job ${msg?.type} failed: `, { error }) + (error: HandlerError) => { + const { code, message, details, stack } = error; + this.logger?.error(`Job ${msg?.type} failed:`, { code, message, details, stack }) + } ); }, handleCommand: (msg: any, context: any, config: any, eventName: string) => { @@ -195,6 +219,11 @@ export class Worker { } }; + handleError(eventName?: string, error?: HandlerError) { + const { code, message, details, stack } = error; + this.logger?.error(`Error while handling event ${eventName}:`, { code, message, details, stack }) + } + async start(cfg?: ServiceConfig, logger?: Logger): Promise { // Load config this._cfg = cfg = cfg ?? createServiceConfig(process.cwd());