@accounter/server 0.0.9-alpha-20251217093036-7168648b507d62946aa287af4ea690b73b077b2d → 0.0.9-alpha-20251217131153-65f961a4072436d7f1042ea8ea4d96534cb3650e
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/CHANGELOG.md +16 -8
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +45 -122
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +45 -29
- package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +2 -11
- package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +25 -0
- package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +65 -64
- package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +494 -60
- package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +34 -98
- package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +79 -59
- package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +6 -4
- package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +9 -2
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +24 -2
- package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +1 -4
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +2 -1
- package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/index.d.ts +0 -1
- package/dist/server/src/modules/charges-matcher/index.js +0 -1
- package/dist/server/src/modules/charges-matcher/index.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +2 -1
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +2 -2
- package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +2 -2
- package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +4 -5
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +5 -4
- package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +5 -3
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +70 -13
- package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +4 -2
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +15 -7
- package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -1
- package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +1 -1
- package/dist/server/src/modules/charges-matcher/types.d.ts +2 -4
- package/dist/server/src/modules/charges-matcher/types.js.map +1 -1
- package/package.json +2 -2
- package/src/modules/charges-matcher/README.md +14 -3
- package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +52 -100
- package/src/modules/charges-matcher/__tests__/auto-match.test.ts +51 -29
- package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +2 -13
- package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +29 -0
- package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +0 -1
- package/src/modules/charges-matcher/__tests__/document-amount.test.ts +66 -65
- package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +552 -60
- package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +43 -73
- package/src/modules/charges-matcher/__tests__/single-match.test.ts +81 -59
- package/src/modules/charges-matcher/documentation/SPEC.md +276 -4
- package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +7 -5
- package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +32 -2
- package/src/modules/charges-matcher/helpers/document-amount.helper.ts +2 -12
- package/src/modules/charges-matcher/index.ts +0 -1
- package/src/modules/charges-matcher/providers/auto-match.provider.ts +5 -3
- package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +8 -2
- package/src/modules/charges-matcher/providers/document-aggregator.ts +12 -11
- package/src/modules/charges-matcher/providers/match-scorer.provider.ts +97 -17
- package/src/modules/charges-matcher/providers/single-match.provider.ts +21 -8
- package/src/modules/charges-matcher/providers/transaction-aggregator.ts +1 -1
- package/src/modules/charges-matcher/types.ts +2 -5
|
@@ -501,7 +501,7 @@ Document date: **Uses `date` field**
|
|
|
501
501
|
|
|
502
502
|
- Aggregation uses latest document `date`
|
|
503
503
|
|
|
504
|
-
**Confidence Calculation:**
|
|
504
|
+
**Confidence Calculation (Standard Formula):**
|
|
505
505
|
|
|
506
506
|
```
|
|
507
507
|
days_diff = |transaction_date - document_date| in days
|
|
@@ -513,9 +513,281 @@ else:
|
|
|
513
513
|
date_conf = 1.0 - (days_diff / 30)
|
|
514
514
|
```
|
|
515
515
|
|
|
516
|
-
**Note:**
|
|
517
|
-
|
|
518
|
-
behavior.
|
|
516
|
+
**Note:** This standard formula applies to **cross-business scenarios and non-client same-business
|
|
517
|
+
matches**. For registered clients with same-business matches, see section 4.3.5 below for
|
|
518
|
+
client-aware date confidence behavior.
|
|
519
|
+
|
|
520
|
+
Simplified from original spec which proposed different date selection per document type. Current
|
|
521
|
+
implementation uses `event_date` for all cases, providing consistent and predictable behavior.
|
|
522
|
+
|
|
523
|
+
#### 4.3.5 Client-Aware Date Confidence (v3.0 - Gentle Scoring)
|
|
524
|
+
|
|
525
|
+
**Enhancement Overview:**
|
|
526
|
+
|
|
527
|
+
Date-confidence calculation now uses "gentle scoring" for eligible client invoices. Instead of
|
|
528
|
+
completely ignoring date differences, the system applies a very subtle linear preference for earlier
|
|
529
|
+
invoices while still maintaining near-maximum confidence. This addresses recurring subscription
|
|
530
|
+
scenarios where clients pay invoices late, while providing a slight edge to match the earliest
|
|
531
|
+
eligible open invoice.
|
|
532
|
+
|
|
533
|
+
**Business Logic:**
|
|
534
|
+
|
|
535
|
+
- **Gentle Eligible Client Match:** When ALL conditions are met:
|
|
536
|
+
- `transaction.business_id` equals `document.creditor_id` or `document.debtor_id` (same business)
|
|
537
|
+
- Business is found in ClientsProvider (registered client)
|
|
538
|
+
- Document status is `OPEN` (via IssuedDocumentsProvider)
|
|
539
|
+
- Document type is `INVOICE` or `PROFORMA`
|
|
540
|
+
- Document date ≤ transaction date (date-only comparison)
|
|
541
|
+
- Days between dates ≤ 365
|
|
542
|
+
|
|
543
|
+
Apply **gentle linear scoring**: f(d) = a + k·d where d = days between dates
|
|
544
|
+
- f(365) = 1.00 (one year earlier gets highest score)
|
|
545
|
+
- f(60) ≈ 0.997 (two months earlier)
|
|
546
|
+
- f(15) ≈ 0.9966 (half-month earlier)
|
|
547
|
+
- f(0) ≈ 0.9964 (same day)
|
|
548
|
+
- If d > 365: return 0.0 (out of boundary)
|
|
549
|
+
|
|
550
|
+
- **Standard Degradation:** Apply to all other cases:
|
|
551
|
+
- Non-client same-business matches
|
|
552
|
+
- Cross-business matches
|
|
553
|
+
- Client matches with ineligible document types (INVOICE_RECEIPT, RECEIPT, CREDIT_INVOICE)
|
|
554
|
+
- Client matches where document date > transaction date
|
|
555
|
+
- Client matches with non-OPEN status
|
|
556
|
+
|
|
557
|
+
Standard formula: 1.0 - (days_diff / 30), floor at 0.0 for ≥30 days
|
|
558
|
+
|
|
559
|
+
**Rationale:**
|
|
560
|
+
|
|
561
|
+
Same-business matches for registered CLIENTS typically represent recurring subscriptions where the
|
|
562
|
+
earliest open invoice should be matched. The gentle scoring provides near-maximum confidence (all
|
|
563
|
+
round to ~1.00 at 2 decimals) while giving a microscopic edge to earlier invoices. Combined with a
|
|
564
|
+
tie-breaker that prefers earlier dates when scores are equal, this ensures transactions match to the
|
|
565
|
+
earliest eligible invoice rather than the latest.
|
|
566
|
+
|
|
567
|
+
Non-client (provider) businesses and ineligible document scenarios maintain standard date-based
|
|
568
|
+
ranking to catch timing mismatches.
|
|
569
|
+
|
|
570
|
+
**Decision Tree:**
|
|
571
|
+
|
|
572
|
+
```
|
|
573
|
+
┌─────────────────────────────────────────┐
|
|
574
|
+
│ Does transaction.business_id equal │
|
|
575
|
+
│ document business ID (creditor/debtor)? │
|
|
576
|
+
└──────────────┬──────────────────────────┘
|
|
577
|
+
│
|
|
578
|
+
┌───────┴───────┐
|
|
579
|
+
│ │
|
|
580
|
+
YES NO
|
|
581
|
+
│ │
|
|
582
|
+
▼ ▼
|
|
583
|
+
┌──────────────┐ ┌──────────────────┐
|
|
584
|
+
│ Is business │ │ Cross-business: │
|
|
585
|
+
│ a registered │ │ Use standard │
|
|
586
|
+
│ client? │ │ degradation │
|
|
587
|
+
└──────┬───────┘ └──────────────────┘
|
|
588
|
+
│
|
|
589
|
+
┌───┴──------------─┐
|
|
590
|
+
│ │
|
|
591
|
+
YES NO
|
|
592
|
+
│ │
|
|
593
|
+
▼ ▼
|
|
594
|
+
┌──────────────┐ ┌──────────────────┐
|
|
595
|
+
│ Check gating │ │ Non-client: │
|
|
596
|
+
│ conditions: │ │ Use standard │
|
|
597
|
+
│ • OPEN │ │ degradation │
|
|
598
|
+
│ • INV/PROF │ └──────────────────┘
|
|
599
|
+
│ • docDate≤tx │
|
|
600
|
+
│ • d≤365 │
|
|
601
|
+
└──────┬───────┘
|
|
602
|
+
│
|
|
603
|
+
┌───┴───------------┐
|
|
604
|
+
│ │
|
|
605
|
+
ALL ANY
|
|
606
|
+
MET FAIL
|
|
607
|
+
│ │
|
|
608
|
+
▼ ▼
|
|
609
|
+
┌──────────┐ ┌──────────────────┐
|
|
610
|
+
│ Gentle │ │ Use standard │
|
|
611
|
+
│ scoring │ │ degradation │
|
|
612
|
+
│ f(d) │ └──────────────────┘
|
|
613
|
+
└──────────┘
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Formula:**
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
function calculateDateConfidence(
|
|
620
|
+
transactionDate: Date,
|
|
621
|
+
documentDate: Date,
|
|
622
|
+
isGentleEligible: boolean = false // All gating conditions met
|
|
623
|
+
): number {
|
|
624
|
+
const daysDiff = calculateDaysDifference(transactionDate, documentDate) // Date-only, absolute
|
|
625
|
+
|
|
626
|
+
// Gentle eligible: linear function with very subtle preference for earlier
|
|
627
|
+
if (isGentleEligible) {
|
|
628
|
+
if (daysDiff > 365) {
|
|
629
|
+
return 0.0 // Out of boundary
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Linear function: f(d) = a + k*d
|
|
633
|
+
// Targets: f(365)=1.0, f(60)=0.997
|
|
634
|
+
const k = (1.0 - 0.997) / (365 - 60) // ≈ 0.0000098360656
|
|
635
|
+
const a = 1.0 - 365 * k // ≈ 0.9964065574
|
|
636
|
+
const confidence = a + k * daysDiff
|
|
637
|
+
|
|
638
|
+
return Math.round(confidence * 100) / 100 // Round to 2 decimals
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Standard degradation for all other cases
|
|
642
|
+
if (daysDiff >= 30) {
|
|
643
|
+
return 0.0
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const confidence = 1.0 - daysDiff / 30
|
|
647
|
+
return Math.round(confidence * 100) / 100
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
**Parameter Details:**
|
|
652
|
+
|
|
653
|
+
- `transactionDate`: Always uses `event_date` from aggregated transaction data
|
|
654
|
+
- `documentDate`: Always uses `date` field from aggregated document data
|
|
655
|
+
- `isGentleEligible`: Boolean flag indicating ALL gating conditions are met:
|
|
656
|
+
- `businessesMatch`: Transaction's `business_id` equals document's counterparty business ID
|
|
657
|
+
- `isClient`: Business is registered in ClientsProvider (loaded via DataLoader)
|
|
658
|
+
- `statusIsOpen`: Document status is 'OPEN' (from IssuedDocumentsProvider by charge ID)
|
|
659
|
+
- `typeEligible`: Document type is INVOICE or PROFORMA
|
|
660
|
+
- `dateDirection`: Document date ≤ transaction date (date-only comparison)
|
|
661
|
+
- Flag is `true` only when ALL conditions are met
|
|
662
|
+
|
|
663
|
+
**Gating Implementation:**
|
|
664
|
+
|
|
665
|
+
Gating is performed in `match-scorer.provider.ts` before calling the date confidence helper:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Check all gating conditions
|
|
669
|
+
const typeIsEligible = document.type === 'INVOICE' || document.type === 'PROFORMA'
|
|
670
|
+
|
|
671
|
+
const docDate = new Date(
|
|
672
|
+
document.date.getFullYear(),
|
|
673
|
+
document.date.getMonth(),
|
|
674
|
+
document.date.getDate()
|
|
675
|
+
)
|
|
676
|
+
const txDate = new Date(
|
|
677
|
+
transactionDate.getFullYear(),
|
|
678
|
+
transactionDate.getMonth(),
|
|
679
|
+
transactionDate.getDate()
|
|
680
|
+
)
|
|
681
|
+
const dateIsEligible = docDate.getTime() <= txDate.getTime()
|
|
682
|
+
|
|
683
|
+
const status = await injector
|
|
684
|
+
.get(IssuedDocumentsProvider)
|
|
685
|
+
.getIssuedDocumentsStatusByChargeIdLoader.load(chargeId)
|
|
686
|
+
const statusIsEligible = !!(status && status.open_docs_flag === true)
|
|
687
|
+
|
|
688
|
+
const isGentleEligible = isClientMatch && typeIsEligible && dateIsEligible && statusIsEligible
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**Overall Confidence Impact:**
|
|
692
|
+
|
|
693
|
+
This change affects the date component of the weighted confidence formula, which carries 10% weight:
|
|
694
|
+
|
|
695
|
+
```
|
|
696
|
+
confidence = (amount × 0.4) + (currency × 0.2) + (business × 0.3) + (date × 0.1)
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
- **Client same-business matches:** Date contributes +0.1 to overall confidence (always max)
|
|
700
|
+
- **Non-client or cross-business matches:** Date contributes 0.0–0.1 depending on time offset
|
|
701
|
+
|
|
702
|
+
**Example Scenario: Recurring Monthly Subscription**
|
|
703
|
+
|
|
704
|
+
**Setup:**
|
|
705
|
+
|
|
706
|
+
- Recurring monthly subscription for $5,000
|
|
707
|
+
- Three invoices: Nov 1, Dec 1, Jan 1 (all $5,000, same client business)
|
|
708
|
+
- One transaction: Dec 28, amount $5,000
|
|
709
|
+
|
|
710
|
+
**Without Client-Aware Enhancement (Standard Degradation):**
|
|
711
|
+
|
|
712
|
+
| Invoice Date | Date Diff | Date Conf | Amount | Currency | Business | **Total** |
|
|
713
|
+
| ------------ | --------- | --------- | ------ | -------- | -------- | --------- |
|
|
714
|
+
| Nov 1 | 57 days | 0.00 | 1.0 | 1.0 | 1.0 | **0.90** |
|
|
715
|
+
| Dec 1 | 27 days | 0.10 | 1.0 | 1.0 | 1.0 | **0.91** |
|
|
716
|
+
| Jan 1 | 4 days | 0.87 | 1.0 | 1.0 | 1.0 | **0.99** |
|
|
717
|
+
|
|
718
|
+
**Result:** Transaction matches to Jan 1 invoice (future date, closest)
|
|
719
|
+
|
|
720
|
+
**With Client-Aware Enhancement (Gentle Scoring, assuming all OPEN INVOICEs before tx date):**
|
|
721
|
+
|
|
722
|
+
| Invoice Date | Days Back | Date Conf | Amount | Currency | Business | **Total** |
|
|
723
|
+
| ------------ | --------- | --------- | ------ | -------- | -------- | --------- |
|
|
724
|
+
| Nov 1 | 57 days | **1.00** | 1.0 | 1.0 | 1.0 | **1.00** |
|
|
725
|
+
| Dec 1 | 27 days | **1.00** | 1.0 | 1.0 | 1.0 | **1.00** |
|
|
726
|
+
| Jan 1\* | N/A | 0.87 | 1.0 | 1.0 | 1.0 | **0.99** |
|
|
727
|
+
|
|
728
|
+
\*Note: Jan 1 is AFTER tx date (Dec 28), so gentle doesn't apply; uses standard degradation.
|
|
729
|
+
|
|
730
|
+
**Result:** Nov 1 and Dec 1 both score 1.00. With gentle mode tie-breaker (prefers earlier), Nov 1
|
|
731
|
+
wins as it's further back in time (57 days > 27 days).
|
|
732
|
+
|
|
733
|
+
**More Realistic Example (all invoices before tx):**
|
|
734
|
+
|
|
735
|
+
Transaction: Feb 15. Open invoices: Nov 15 (92d), Dec 15 (62d), Jan 15 (31d)
|
|
736
|
+
|
|
737
|
+
| Invoice Date | Days Back | Raw f(d) | Rounded | Amount | Currency | Business | **Total** |
|
|
738
|
+
| ------------ | --------- | -------- | -------- | ------ | -------- | -------- | --------- |
|
|
739
|
+
| Nov 15 | 92 days | 0.99731 | **1.00** | 1.0 | 1.0 | 1.0 | **1.00** |
|
|
740
|
+
| Dec 15 | 62 days | 0.99701 | **1.00** | 1.0 | 1.0 | 1.0 | **1.00** |
|
|
741
|
+
| Jan 15 | 31 days | 0.99671 | **1.00** | 1.0 | 1.0 | 1.0 | **1.00** |
|
|
742
|
+
|
|
743
|
+
**Result:** All round to 1.00 at 2 decimals. Gentle tie-breaker prefers earlier → **Nov 15** wins.
|
|
744
|
+
|
|
745
|
+
**Comparison Table: Gentle vs Standard Behavior**
|
|
746
|
+
|
|
747
|
+
| Scenario | Gating Met | Days Back | Raw f(d) | Date Conf (2dp) | Logic Applied |
|
|
748
|
+
| --------------------------------- | ---------- | --------- | -------- | --------------- | --------------------- |
|
|
749
|
+
| Client OPEN INV, docDate≤tx (0) | ✓ | 0 days | 0.99641 | 1.00 | Gentle |
|
|
750
|
+
| Client OPEN INV, docDate≤tx (15) | ✓ | 15 days | 0.99656 | 1.00 | Gentle |
|
|
751
|
+
| Client OPEN INV, docDate≤tx (60) | ✓ | 60 days | 0.99700 | 1.00 | Gentle |
|
|
752
|
+
| Client OPEN INV, docDate≤tx (365) | ✓ | 365 days | 1.00000 | 1.00 | Gentle (max) |
|
|
753
|
+
| Client OPEN INV, docDate≤tx (366) | ✗ | 366 days | N/A | 0.00 | Gentle (out of bound) |
|
|
754
|
+
| Client OPEN INV, docDate>tx | ✗ | 15 days | N/A | 0.50 | Standard (ineligible) |
|
|
755
|
+
| Client OPEN RECEIPT | ✗ | 15 days | N/A | 0.50 | Standard (wrong type) |
|
|
756
|
+
| Client PAID INV, docDate≤tx | ✗ | 15 days | N/A | 0.50 | Standard (not OPEN) |
|
|
757
|
+
| Same-Business Provider (0) | ✗ | 0 days | N/A | 1.00 | Standard degradation |
|
|
758
|
+
| Same-Business Provider (15) | ✗ | 15 days | N/A | 0.50 | Standard degradation |
|
|
759
|
+
| Same-Business Provider (30+) | ✗ | 30+ days | N/A | 0.00 | Standard degradation |
|
|
760
|
+
| Cross-Business (0) | ✗ | 0 days | N/A | 1.00 | Standard degradation |
|
|
761
|
+
| Cross-Business (15) | ✗ | 15 days | N/A | 0.50 | Standard degradation |
|
|
762
|
+
| Cross-Business (30+) | ✗ | 30+ days | N/A | 0.00 | Standard degradation |
|
|
763
|
+
|
|
764
|
+
**Key Observations:**
|
|
765
|
+
|
|
766
|
+
- **Gentle-eligible matches** (client, OPEN, INVOICE/PROFORMA, docDate≤tx, d≤365) receive ~1.00
|
|
767
|
+
confidence with microscopic preference for earlier dates
|
|
768
|
+
- **All ineligible scenarios** use standard degradation (1.0 at 0 days → 0.0 at 30+ days)
|
|
769
|
+
- Gentle scoring boundary at >365 days returns 0.0
|
|
770
|
+
- **Tie-breaker:** When gentle scores are equal (both ~1.00), prefer the earlier invoice
|
|
771
|
+
- This enhancement affects only the 10% date component of the overall confidence score
|
|
772
|
+
|
|
773
|
+
**Implementation Details:**
|
|
774
|
+
|
|
775
|
+
- **ClientsProvider Integration:** Uses existing `ClientsProvider` from financial-entities module
|
|
776
|
+
- **IssuedDocumentsProvider Integration:** Uses `getIssuedDocumentsStatusByChargeIdLoader` to check
|
|
777
|
+
OPEN status per charge
|
|
778
|
+
- **DataLoader Pattern:** Business and status lookups use DataLoaders for efficient batch loading
|
|
779
|
+
- **Tie-Breaker:** When both candidates use gentle scoring and confidence scores are equal:
|
|
780
|
+
- Propagate `gentleMode` flag from scorer to single-match provider
|
|
781
|
+
- In sorting, flip preference to larger day gaps (earlier documents) instead of smaller gaps
|
|
782
|
+
- Standard tie-breaker (prefer closer dates) applies to non-gentle or mixed scenarios
|
|
783
|
+
- **Document Type Gating:** PROFORMA removed from accounting document types in charge-validator to
|
|
784
|
+
enable gentle scoring eligibility
|
|
785
|
+
- **Optimization:** Client check only performed when `businessesMatch = true` (avoids unnecessary
|
|
786
|
+
lookups)
|
|
787
|
+
- **Default Behavior:** If business not found in ClientsProvider, `isClient = false` (standard
|
|
788
|
+
degradation)
|
|
789
|
+
- **Backward Compatibility:** Optional parameter ensures existing code continues to work with
|
|
790
|
+
standard degradation
|
|
519
791
|
|
|
520
792
|
### 4.4 Sorting and Selection
|
|
521
793
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DocumentType } from '../../../shared/enums.js';
|
|
2
|
+
import type { Document, Transaction } from '../types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Represents a charge with its associated transactions and documents
|
|
@@ -14,10 +15,11 @@ interface Charge {
|
|
|
14
15
|
* Accounting document types that count toward matched/unmatched status
|
|
15
16
|
*/
|
|
16
17
|
const ACCOUNTING_DOC_TYPES: DocumentType[] = [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
DocumentType.Invoice,
|
|
19
|
+
DocumentType.CreditInvoice,
|
|
20
|
+
DocumentType.Receipt,
|
|
21
|
+
DocumentType.InvoiceReceipt,
|
|
22
|
+
DocumentType.Proforma,
|
|
21
23
|
];
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -21,11 +21,41 @@ function calculateDaysDifference(date1: Date, date2: Date): number {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Calculate confidence score based on date proximity
|
|
24
|
+
*
|
|
25
|
+
* Client-Aware Logic:
|
|
26
|
+
* - Client same-business matches: Always return 1.0
|
|
27
|
+
* This allows amount/currency matching to be primary ranking factors for recurring charges from clients
|
|
28
|
+
* - Non-client or cross-business matches: Apply linear degradation (1.0 same-day to 0.0 at 30+ days)
|
|
29
|
+
*
|
|
24
30
|
* @param date1 - First date
|
|
25
31
|
* @param date2 - Second date
|
|
26
|
-
* @
|
|
32
|
+
* @param isClientMatch - Whether transaction business matches document business AND is a registered CLIENT (default: false)
|
|
33
|
+
* @returns Confidence score from 0.0 (30+ days) to 1.0 (same day for non-client, always for client)
|
|
27
34
|
*/
|
|
28
|
-
export function calculateDateConfidence(
|
|
35
|
+
export function calculateDateConfidence(
|
|
36
|
+
date1: Date,
|
|
37
|
+
date2: Date,
|
|
38
|
+
isGentleEligible: boolean = false,
|
|
39
|
+
): number {
|
|
40
|
+
// Gentle client-eligible scoring: slight preference for earlier within 365 days
|
|
41
|
+
// f(d) = a + k*d with f(365)=1.0 and f(60)=0.997
|
|
42
|
+
if (isGentleEligible) {
|
|
43
|
+
const daysDiff = calculateDaysDifference(date1, date2);
|
|
44
|
+
|
|
45
|
+
// Ineligible beyond 365 days
|
|
46
|
+
if (daysDiff > 365) {
|
|
47
|
+
return 0.0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Compute linear function parameters
|
|
51
|
+
const k = (1.0 - 0.997) / (365 - 60); // ~9.836e-6
|
|
52
|
+
const a = 0.997 - k * 60; // ~0.99640984
|
|
53
|
+
const confidence = a + k * daysDiff;
|
|
54
|
+
|
|
55
|
+
// Round to 2 decimal places (will be 1.0 for most practical diffs)
|
|
56
|
+
return Math.round(confidence * 100) / 100;
|
|
57
|
+
}
|
|
58
|
+
|
|
29
59
|
const daysDiff = calculateDaysDifference(date1, date2);
|
|
30
60
|
|
|
31
61
|
// 30 or more days: return 0.0
|
|
@@ -8,17 +8,7 @@
|
|
|
8
8
|
* 3. If document type is CREDIT_INVOICE: negate
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
* Document types from database schema
|
|
13
|
-
*/
|
|
14
|
-
export type DocumentType =
|
|
15
|
-
| 'CREDIT_INVOICE'
|
|
16
|
-
| 'INVOICE'
|
|
17
|
-
| 'INVOICE_RECEIPT'
|
|
18
|
-
| 'OTHER'
|
|
19
|
-
| 'PROFORMA'
|
|
20
|
-
| 'RECEIPT'
|
|
21
|
-
| 'UNPROCESSED';
|
|
11
|
+
import { DocumentType } from '../../../shared/enums.js';
|
|
22
12
|
|
|
23
13
|
/**
|
|
24
14
|
* Normalize document amount for comparison with transaction amount
|
|
@@ -69,7 +59,7 @@ export function normalizeDocumentAmount(
|
|
|
69
59
|
}
|
|
70
60
|
|
|
71
61
|
// Step 3: If document type is CREDIT_INVOICE, negate
|
|
72
|
-
if (documentType ===
|
|
62
|
+
if (documentType === DocumentType.CreditInvoice) {
|
|
73
63
|
normalizedAmount = -normalizedAmount;
|
|
74
64
|
}
|
|
75
65
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and automatically merging charges with high-confidence matches (≥0.95).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Injector } from 'graphql-modules';
|
|
8
9
|
import type { ChargeWithData, DocumentCharge, TransactionCharge } from '../types.js';
|
|
9
10
|
import { findMatches, type MatchResult } from './single-match.provider.js';
|
|
10
11
|
|
|
@@ -35,11 +36,12 @@ export interface ProcessChargeResult {
|
|
|
35
36
|
* @throws Error if sourceCharge has no transactions or documents
|
|
36
37
|
* @throws Error if any validation fails (propagated from findMatches)
|
|
37
38
|
*/
|
|
38
|
-
export function processChargeForAutoMatch(
|
|
39
|
+
export async function processChargeForAutoMatch(
|
|
39
40
|
sourceCharge: ChargeWithData,
|
|
40
41
|
allCandidates: ChargeWithData[],
|
|
41
42
|
userId: string,
|
|
42
|
-
|
|
43
|
+
injector: Injector,
|
|
44
|
+
): Promise<ProcessChargeResult> {
|
|
43
45
|
const AUTO_MATCH_THRESHOLD = 0.95;
|
|
44
46
|
|
|
45
47
|
// Prepare source charge for findMatches
|
|
@@ -92,7 +94,7 @@ export function processChargeForAutoMatch(
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
// Find all matches with no date window restriction
|
|
95
|
-
const allMatches = findMatches(sourceForMatching, candidatesForMatching, userId, {
|
|
97
|
+
const allMatches = await findMatches(sourceForMatching, candidatesForMatching, userId, injector, {
|
|
96
98
|
dateWindowMonths: undefined, // No date restriction for auto-match
|
|
97
99
|
maxMatches: undefined, // Get all matches, we'll filter by threshold
|
|
98
100
|
});
|
|
@@ -169,10 +169,11 @@ export class ChargesMatcherProvider {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Step 8: Call core findMatches function
|
|
172
|
-
const matches: MatchResult[] = findMatches(
|
|
172
|
+
const matches: MatchResult[] = await findMatches(
|
|
173
173
|
sourceChargeData,
|
|
174
174
|
candidateChargesWithData,
|
|
175
175
|
adminBusinessId,
|
|
176
|
+
injector,
|
|
176
177
|
{
|
|
177
178
|
maxMatches: 5,
|
|
178
179
|
dateWindowMonths: 12,
|
|
@@ -278,7 +279,12 @@ export class ChargesMatcherProvider {
|
|
|
278
279
|
);
|
|
279
280
|
|
|
280
281
|
// Process this charge for auto-match
|
|
281
|
-
const processResult = processChargeForAutoMatch(
|
|
282
|
+
const processResult = await processChargeForAutoMatch(
|
|
283
|
+
sourceCharge,
|
|
284
|
+
candidates,
|
|
285
|
+
adminBusinessId,
|
|
286
|
+
injector,
|
|
287
|
+
);
|
|
282
288
|
|
|
283
289
|
if (processResult.status === 'matched' && processResult.match) {
|
|
284
290
|
// Found a single high-confidence match - execute merge
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* amount normalization, currency validation, date selection, and description concatenation.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { DocumentType } from '../../../shared/enums.js';
|
|
10
|
+
import { currency, document_type } from '../../documents/types.js';
|
|
11
|
+
import { normalizeDocumentAmount } from '../helpers/document-amount.helper.js';
|
|
11
12
|
import { extractDocumentBusiness } from '../helpers/document-business.helper.js';
|
|
12
13
|
import { AggregatedDocument } from '../types.js';
|
|
13
14
|
|
|
@@ -24,7 +25,7 @@ export interface Document {
|
|
|
24
25
|
currency_code: currency | null; // Currency type
|
|
25
26
|
date: Date | null;
|
|
26
27
|
total_amount: number | null; // double precision in DB
|
|
27
|
-
type:
|
|
28
|
+
type: document_type;
|
|
28
29
|
serial_number: string | null;
|
|
29
30
|
image_url: string | null;
|
|
30
31
|
file_url: string | null;
|
|
@@ -33,8 +34,8 @@ export interface Document {
|
|
|
33
34
|
/**
|
|
34
35
|
* Accounting document types (used for type priority filtering)
|
|
35
36
|
*/
|
|
36
|
-
const INVOICE_TYPES: DocumentType[] = [
|
|
37
|
-
const RECEIPT_TYPES: DocumentType[] = [
|
|
37
|
+
const INVOICE_TYPES: DocumentType[] = [DocumentType.Invoice, DocumentType.CreditInvoice];
|
|
38
|
+
const RECEIPT_TYPES: DocumentType[] = [DocumentType.Receipt, DocumentType.InvoiceReceipt];
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Check if document type is an invoice or credit invoice
|
|
@@ -77,8 +78,8 @@ function isReceiptType(type: DocumentType): boolean {
|
|
|
77
78
|
*
|
|
78
79
|
* @example
|
|
79
80
|
* const aggregated = aggregateDocuments([
|
|
80
|
-
* { total_amount: 100, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type:
|
|
81
|
-
* { total_amount: 50, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type:
|
|
81
|
+
* { total_amount: 100, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: DocumentType.Invoice, ... },
|
|
82
|
+
* { total_amount: 50, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: DocumentType.Invoice, ... }
|
|
82
83
|
* ], 'u1');
|
|
83
84
|
* // Returns aggregated with normalized amounts summed
|
|
84
85
|
*/
|
|
@@ -92,13 +93,13 @@ export function aggregateDocuments(
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// Apply type priority filtering
|
|
95
|
-
const hasInvoices = documents.some(d => isInvoiceType(d.type));
|
|
96
|
-
const hasReceipts = documents.some(d => isReceiptType(d.type));
|
|
96
|
+
const hasInvoices = documents.some(d => isInvoiceType(d.type as DocumentType));
|
|
97
|
+
const hasReceipts = documents.some(d => isReceiptType(d.type as DocumentType));
|
|
97
98
|
|
|
98
99
|
let filteredDocuments = documents;
|
|
99
100
|
if (hasInvoices && hasReceipts) {
|
|
100
101
|
// If both invoices and receipts exist, use only invoices
|
|
101
|
-
filteredDocuments = documents.filter(d => isInvoiceType(d.type));
|
|
102
|
+
filteredDocuments = documents.filter(d => isInvoiceType(d.type as DocumentType));
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
// Validate we still have documents after filtering
|
|
@@ -112,7 +113,7 @@ export function aggregateDocuments(
|
|
|
112
113
|
const normalizedAmount = normalizeDocumentAmount(
|
|
113
114
|
doc.total_amount ?? 0,
|
|
114
115
|
businessInfo.isBusinessCreditor,
|
|
115
|
-
doc.type,
|
|
116
|
+
doc.type as DocumentType,
|
|
116
117
|
);
|
|
117
118
|
|
|
118
119
|
return {
|