@open-mercato/core 0.6.5-develop.4490.1.d8e873f3cf → 0.6.5-develop.4516.1.88e6ab71a9

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.
@@ -294,33 +294,38 @@ const createPersonCompanyLinkCommand: CommandHandler<PersonCompanyLinkCreateInpu
294
294
  )
295
295
  if (!link) return
296
296
 
297
+ let person: CustomerEntity | null = null
298
+ let profile: CustomerPersonProfile | null = null
299
+ let remainingLinks: CustomerPersonCompanyLink[] = []
300
+
297
301
  await withAtomicFlush(em, [
298
302
  async () => {
299
- link.isPrimary = false
300
- link.deletedAt = new Date()
301
- const person = await findOneWithDecryption(
303
+ person = await findOneWithDecryption(
302
304
  em,
303
305
  CustomerEntity,
304
306
  { id: after.personEntityId, kind: 'person', tenantId: after.tenantId, organizationId: after.organizationId, deletedAt: null },
305
307
  undefined,
306
308
  { tenantId: after.tenantId, organizationId: after.organizationId },
307
309
  )
308
- if (person) {
309
- const profile = await findOneWithDecryption(
310
- em,
311
- CustomerPersonProfile,
312
- { entity: person },
313
- { populate: ['company'] },
314
- { tenantId: person.tenantId, organizationId: person.organizationId },
315
- )
316
- if (profile) {
317
- const remainingLinks = await loadPersonCompanyLinks(em, person)
318
- if (after.isPrimary) {
319
- await promoteFallbackPrimaryLink(em, person, profile, remainingLinks, after.companyEntityId)
320
- } else if (profile.company && typeof profile.company !== 'string' && profile.company.id === after.companyEntityId) {
321
- profile.company = null
322
- }
323
- }
310
+ if (!person) return
311
+ profile = await findOneWithDecryption(
312
+ em,
313
+ CustomerPersonProfile,
314
+ { entity: person },
315
+ { populate: ['company'] },
316
+ { tenantId: person.tenantId, organizationId: person.organizationId },
317
+ )
318
+ if (!profile) return
319
+ remainingLinks = (await loadPersonCompanyLinks(em, person)).filter((entry) => entry.id !== link.id)
320
+ },
321
+ async () => {
322
+ link.isPrimary = false
323
+ link.deletedAt = new Date()
324
+ if (!person || !profile) return
325
+ if (after.isPrimary) {
326
+ await promoteFallbackPrimaryLink(em, person, profile, remainingLinks, after.companyEntityId)
327
+ } else if (profile.company && typeof profile.company !== 'string' && profile.company.id === after.companyEntityId) {
328
+ profile.company = null
324
329
  }
325
330
  },
326
331
  ], { transaction: true })
@@ -453,39 +458,44 @@ const updatePersonCompanyLinkCommand: CommandHandler<PersonCompanyLinkUpdateInpu
453
458
  )
454
459
  if (!link) return
455
460
 
461
+ let person: CustomerEntity | null = null
462
+ let profile: CustomerPersonProfile | null = null
463
+ let company: CustomerEntity | null = null
464
+
456
465
  await withAtomicFlush(em, [
457
466
  async () => {
458
- const person = await findOneWithDecryption(
467
+ person = await findOneWithDecryption(
459
468
  em,
460
469
  CustomerEntity,
461
470
  { id: before.personEntityId, kind: 'person', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
462
471
  undefined,
463
472
  { tenantId: before.tenantId, organizationId: before.organizationId },
464
473
  )
465
- if (person) {
466
- const profile = await findOneWithDecryption(
467
- em,
468
- CustomerPersonProfile,
469
- { entity: person },
470
- { populate: ['company'] },
471
- { tenantId: person.tenantId, organizationId: person.organizationId },
472
- )
473
- if (profile) {
474
- if (before.isPrimary) {
475
- await clearPrimaryFlagsForPerson(em, person)
476
- link.isPrimary = true
477
- const company = await findOneWithDecryption(
478
- em,
479
- CustomerEntity,
480
- { id: before.companyEntityId, kind: 'company', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
481
- undefined,
482
- { tenantId: before.tenantId, organizationId: before.organizationId },
483
- )
484
- if (company) profile.company = company
485
- } else {
486
- link.isPrimary = false
487
- }
488
- }
474
+ if (!person) return
475
+ profile = await findOneWithDecryption(
476
+ em,
477
+ CustomerPersonProfile,
478
+ { entity: person },
479
+ { populate: ['company'] },
480
+ { tenantId: person.tenantId, organizationId: person.organizationId },
481
+ )
482
+ if (!profile || !before.isPrimary) return
483
+ company = await findOneWithDecryption(
484
+ em,
485
+ CustomerEntity,
486
+ { id: before.companyEntityId, kind: 'company', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
487
+ undefined,
488
+ { tenantId: before.tenantId, organizationId: before.organizationId },
489
+ )
490
+ },
491
+ async () => {
492
+ if (!person || !profile) return
493
+ if (before.isPrimary) {
494
+ await clearPrimaryFlagsForPerson(em, person)
495
+ link.isPrimary = true
496
+ if (company) profile.company = company
497
+ } else {
498
+ link.isPrimary = false
489
499
  }
490
500
  },
491
501
  ], { transaction: true })
@@ -610,38 +620,43 @@ const deletePersonCompanyLinkCommand: CommandHandler<PersonCompanyLinkDeleteInpu
610
620
  )
611
621
  if (!link) return
612
622
 
623
+ let person: CustomerEntity | null = null
624
+ let profile: CustomerPersonProfile | null = null
625
+ let company: CustomerEntity | null = null
626
+
613
627
  await withAtomicFlush(em, [
614
628
  async () => {
615
- link.deletedAt = null
616
- link.isPrimary = before.isPrimary
617
-
618
- const person = await findOneWithDecryption(
629
+ person = await findOneWithDecryption(
619
630
  em,
620
631
  CustomerEntity,
621
632
  { id: before.personEntityId, kind: 'person', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
622
633
  undefined,
623
634
  { tenantId: before.tenantId, organizationId: before.organizationId },
624
635
  )
636
+ if (!person || !before.isPrimary) return
637
+ profile = await findOneWithDecryption(
638
+ em,
639
+ CustomerPersonProfile,
640
+ { entity: person },
641
+ { populate: ['company'] },
642
+ { tenantId: person.tenantId, organizationId: person.organizationId },
643
+ )
644
+ if (!profile) return
645
+ company = await findOneWithDecryption(
646
+ em,
647
+ CustomerEntity,
648
+ { id: before.companyEntityId, kind: 'company', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
649
+ undefined,
650
+ { tenantId: before.tenantId, organizationId: before.organizationId },
651
+ )
652
+ },
653
+ async () => {
654
+ link.deletedAt = null
655
+ link.isPrimary = before.isPrimary
625
656
  if (person && before.isPrimary) {
626
657
  await clearPrimaryFlagsForPerson(em, person)
627
658
  link.isPrimary = true
628
- const profile = await findOneWithDecryption(
629
- em,
630
- CustomerPersonProfile,
631
- { entity: person },
632
- { populate: ['company'] },
633
- { tenantId: person.tenantId, organizationId: person.organizationId },
634
- )
635
- if (profile) {
636
- const company = await findOneWithDecryption(
637
- em,
638
- CustomerEntity,
639
- { id: before.companyEntityId, kind: 'company', tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
640
- undefined,
641
- { tenantId: before.tenantId, organizationId: before.organizationId },
642
- )
643
- if (company) profile.company = company
644
- }
659
+ if (profile && company) profile.company = company
645
660
  }
646
661
  },
647
662
  ], { transaction: true })
@@ -757,6 +757,12 @@ const updatePaymentCommand: CommandHandler<
757
757
  }
758
758
  payment.updatedAt = new Date()
759
759
 
760
+ // Persist the payment scalar changes before any allocation query below.
761
+ // MikroORM discards pending scalar mutations when a find() runs on the
762
+ // same EntityManager before they are flushed, which would otherwise drop
763
+ // the updated amount/reference when allocations are (re)synced.
764
+ await tx.flush()
765
+
760
766
  if (input.allocations !== undefined) {
761
767
  const existingAllocations = await findWithDecryption(tx, SalesPaymentAllocation, { payment }, {}, { tenantId: payment.tenantId, organizationId: payment.organizationId })
762
768
  existingAllocations.forEach((allocation) => tx.remove(allocation))
@@ -816,6 +822,29 @@ const updatePaymentCommand: CommandHandler<
816
822
  })
817
823
  tx.persist(entity)
818
824
  }
825
+ } else if (input.amount !== undefined || input.currencyCode !== undefined) {
826
+ // The caller changed the payment amount/currency without managing
827
+ // allocations explicitly. A simple payment carries a single
828
+ // auto-created allocation covering the full amount (see create); keep
829
+ // it in sync so recomputeOrderPaymentTotals — which sums allocations in
830
+ // preference to the payment amount — does not report a stale paid total
831
+ // after a payment edit (#2455).
832
+ const existingAllocations = await findWithDecryption(tx, SalesPaymentAllocation, { payment }, {}, { tenantId: payment.tenantId, organizationId: payment.organizationId })
833
+ const paymentOrderId =
834
+ (typeof payment.order === 'string' ? payment.order : payment.order?.id) ?? null
835
+ const isDefaultAllocation = (allocation: SalesPaymentAllocation): boolean => {
836
+ const allocationOrderId =
837
+ typeof allocation.order === 'string' ? allocation.order : allocation.order?.id ?? null
838
+ const allocationInvoiceId =
839
+ typeof allocation.invoice === 'string' ? allocation.invoice : allocation.invoice?.id ?? null
840
+ return allocationInvoiceId === null && allocationOrderId === paymentOrderId
841
+ }
842
+ if (existingAllocations.length === 1 && isDefaultAllocation(existingAllocations[0])) {
843
+ const [allocation] = existingAllocations
844
+ allocation.amount = toNumericString(toNumber(payment.amount)) ?? '0'
845
+ allocation.currencyCode = payment.currencyCode
846
+ tx.persist(allocation)
847
+ }
819
848
  }
820
849
  })
821
850
 
@@ -94,14 +94,23 @@ function buildBaseLineResult(line: SalesLineSnapshot): SalesLineCalculationResul
94
94
  const netSubtotalBeforeDiscount = toNumber(unitNet, 0) * quantity
95
95
  const discountTotal = Math.min(Math.max(discountPerUnit * quantity, 0), netSubtotalBeforeDiscount)
96
96
  const netSubtotal = Math.max(netSubtotalBeforeDiscount - discountTotal, 0)
97
- const taxAmount =
98
- line.taxAmount !== null && line.taxAmount !== undefined
99
- ? toNumber(line.taxAmount, 0)
100
- : round(netSubtotal * Math.max(taxRate, 0))
97
+ const explicitTaxAmount = line.taxAmount !== null && line.taxAmount !== undefined
98
+ let taxAmount = explicitTaxAmount
99
+ ? toNumber(line.taxAmount, 0)
100
+ : round(netSubtotal * Math.max(taxRate, 0))
101
101
  const grossSubtotal =
102
102
  line.totalGrossAmount !== null && line.totalGrossAmount !== undefined
103
103
  ? toNumber(line.totalGrossAmount, 0)
104
104
  : round(netSubtotal + taxAmount)
105
+ // When tax was not supplied explicitly and the rate-derived tax is zero but
106
+ // the gross total already embeds tax (gross > net) — e.g. a tax-class-priced
107
+ // line whose resolved rate was not persisted — derive the tax from the
108
+ // net/gross delta so the document-level tax total is not silently zeroed
109
+ // while per-line net/gross stay correct (#2457).
110
+ if (!explicitTaxAmount && taxAmount <= 0) {
111
+ const grossNetDelta = round(grossSubtotal - netSubtotal)
112
+ if (grossNetDelta > 0) taxAmount = grossNetDelta
113
+ }
105
114
 
106
115
  return {
107
116
  line,
@@ -356,6 +365,25 @@ class SalesCalculationRegistry {
356
365
  })
357
366
  }
358
367
 
368
+ // Payment totals (paid/refunded) are authoritative inputs, not derived from
369
+ // lines or adjustments. Totals calculators rebuild the document result from
370
+ // lines+adjustments and would otherwise reset paid/refunded to 0 (and
371
+ // outstanding back to the full grand total), producing a stale paid/
372
+ // outstanding display after a payment. Re-apply the input totals last and
373
+ // recompute outstanding against the post-calculation grand total.
374
+ if (existingTotals) {
375
+ const paidTotalAmount = Math.max(toNumber(existingTotals.paidTotalAmount, 0), 0)
376
+ const refundedTotalAmount = Math.max(toNumber(existingTotals.refundedTotalAmount, 0), 0)
377
+ current.totals = {
378
+ ...current.totals,
379
+ paidTotalAmount,
380
+ refundedTotalAmount,
381
+ outstandingAmount: round(
382
+ Math.max(current.totals.grandTotalGrossAmount - paidTotalAmount + refundedTotalAmount, 0)
383
+ ),
384
+ }
385
+ }
386
+
359
387
  return current
360
388
  }
361
389
  }