瀏覽代碼

fix(core): Handle foreign key violations during order merge (#3795)

gabriellbui 4 月之前
父節點
當前提交
c00a0441e9

+ 15 - 0
packages/core/src/service/helpers/utils/db-errors.ts

@@ -0,0 +1,15 @@
+/**
+ * Returns true if the given error represents a foreign key constraint violation
+ * across supported drivers (Postgres, MySQL/MariaDB, SQLite).
+ */
+export function isForeignKeyViolationError(e: unknown): boolean {
+    const err: any = e || {};
+    const code = err.code ?? err.driverError?.code ?? err.errno ?? err.driverError?.errno;
+
+    // Postgres: 23503, MySQL/MariaDB: 1451/1452, SQLite: SQLITE_CONSTRAINT_FOREIGNKEY,
+    if (code === '23503' || code === 1451 || code === 1452 || code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
+        return true;
+    }
+    const msg = String(err.message ?? err.driverError?.message ?? '');
+    return /\bforeign key\b/i.test(msg);
+}

+ 33 - 1
packages/core/src/service/services/order.service.ts

@@ -117,6 +117,7 @@ import { RefundState } from '../helpers/refund-state-machine/refund-state';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { TranslatorService } from '../helpers/translator/translator.service';
+import { isForeignKeyViolationError } from '../helpers/utils/db-errors';
 import { getOrdersFromLines, totalCoveredByPayments } from '../helpers/utils/order-utils';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -1889,7 +1890,38 @@ export class OrderService {
         const { orderToDelete, linesToInsert, linesToDelete, linesToModify } = mergeResult;
         let { order } = mergeResult;
         if (orderToDelete) {
-            await this.deleteOrder(ctx, orderToDelete);
+            try {
+                // Separate transaction to isolate foreign key failure, so it doesn't roll back the entire outer transaction
+                await this.connection.withTransaction(ctx, async innerCtx => {
+                    await this.deleteOrder(innerCtx, orderToDelete);
+                });
+            } catch (e: any) {
+                if (!isForeignKeyViolationError(e)) throw e;
+                if (!order)
+                    throw new Error(
+                        `Cannot complete order merge: active order not found, while cancelling order ${orderToDelete.id}`,
+                    );
+
+                // If the order has a foreign key violation (e.g. with cancelled payments),
+                // instead of deleting it we cancel the order and leave a note with an explanation.
+                // This way the previous order and all its information are preserved.
+                await this.cancelOrder(ctx, { orderId: orderToDelete.id });
+
+                const note = [
+                    'This order was cancelled during user sign-in because merging with the active order was not possible.',
+                    `The active order is ${order.code}. This order has been preserved for reference.`,
+                ].join(' ');
+
+                await this.historyService.createHistoryEntryForOrder(
+                    {
+                        ctx,
+                        orderId: orderToDelete.id,
+                        type: HistoryEntryType.ORDER_NOTE,
+                        data: { note },
+                    },
+                    false,
+                );
+            }
         }
         if (order && linesToDelete) {
             const orderId = order.id;