@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +16 -8
  2. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js +45 -122
  3. package/dist/server/src/modules/charges-matcher/__tests__/auto-match-integration.test.js.map +1 -1
  4. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js +45 -29
  5. package/dist/server/src/modules/charges-matcher/__tests__/auto-match.test.js.map +1 -1
  6. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js +2 -11
  7. package/dist/server/src/modules/charges-matcher/__tests__/charge-validator.test.js.map +1 -1
  8. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js +25 -0
  9. package/dist/server/src/modules/charges-matcher/__tests__/date-confidence.test.js.map +1 -1
  10. package/dist/server/src/modules/charges-matcher/__tests__/document-aggregator.test.js.map +1 -1
  11. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js +65 -64
  12. package/dist/server/src/modules/charges-matcher/__tests__/document-amount.test.js.map +1 -1
  13. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js +494 -60
  14. package/dist/server/src/modules/charges-matcher/__tests__/match-scorer.test.js.map +1 -1
  15. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js +34 -98
  16. package/dist/server/src/modules/charges-matcher/__tests__/single-match-integration.test.js.map +1 -1
  17. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js +79 -59
  18. package/dist/server/src/modules/charges-matcher/__tests__/single-match.test.js.map +1 -1
  19. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js +6 -4
  20. package/dist/server/src/modules/charges-matcher/helpers/charge-validator.helper.js.map +1 -1
  21. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.d.ts +9 -2
  22. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js +24 -2
  23. package/dist/server/src/modules/charges-matcher/helpers/date-confidence.helper.js.map +1 -1
  24. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.d.ts +1 -4
  25. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js +2 -1
  26. package/dist/server/src/modules/charges-matcher/helpers/document-amount.helper.js.map +1 -1
  27. package/dist/server/src/modules/charges-matcher/index.d.ts +0 -1
  28. package/dist/server/src/modules/charges-matcher/index.js +0 -1
  29. package/dist/server/src/modules/charges-matcher/index.js.map +1 -1
  30. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.d.ts +2 -1
  31. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js +2 -2
  32. package/dist/server/src/modules/charges-matcher/providers/auto-match.provider.js.map +1 -1
  33. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js +2 -2
  34. package/dist/server/src/modules/charges-matcher/providers/charges-matcher.provider.js.map +1 -1
  35. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.d.ts +4 -5
  36. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js +5 -4
  37. package/dist/server/src/modules/charges-matcher/providers/document-aggregator.js.map +1 -1
  38. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.d.ts +5 -3
  39. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js +70 -13
  40. package/dist/server/src/modules/charges-matcher/providers/match-scorer.provider.js.map +1 -1
  41. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.d.ts +4 -2
  42. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js +15 -7
  43. package/dist/server/src/modules/charges-matcher/providers/single-match.provider.js.map +1 -1
  44. package/dist/server/src/modules/charges-matcher/providers/transaction-aggregator.d.ts +1 -1
  45. package/dist/server/src/modules/charges-matcher/types.d.ts +2 -4
  46. package/dist/server/src/modules/charges-matcher/types.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/modules/charges-matcher/README.md +14 -3
  49. package/src/modules/charges-matcher/__tests__/auto-match-integration.test.ts +52 -100
  50. package/src/modules/charges-matcher/__tests__/auto-match.test.ts +51 -29
  51. package/src/modules/charges-matcher/__tests__/charge-validator.test.ts +2 -13
  52. package/src/modules/charges-matcher/__tests__/date-confidence.test.ts +29 -0
  53. package/src/modules/charges-matcher/__tests__/document-aggregator.test.ts +0 -1
  54. package/src/modules/charges-matcher/__tests__/document-amount.test.ts +66 -65
  55. package/src/modules/charges-matcher/__tests__/match-scorer.test.ts +552 -60
  56. package/src/modules/charges-matcher/__tests__/single-match-integration.test.ts +43 -73
  57. package/src/modules/charges-matcher/__tests__/single-match.test.ts +81 -59
  58. package/src/modules/charges-matcher/documentation/SPEC.md +276 -4
  59. package/src/modules/charges-matcher/helpers/charge-validator.helper.ts +7 -5
  60. package/src/modules/charges-matcher/helpers/date-confidence.helper.ts +32 -2
  61. package/src/modules/charges-matcher/helpers/document-amount.helper.ts +2 -12
  62. package/src/modules/charges-matcher/index.ts +0 -1
  63. package/src/modules/charges-matcher/providers/auto-match.provider.ts +5 -3
  64. package/src/modules/charges-matcher/providers/charges-matcher.provider.ts +8 -2
  65. package/src/modules/charges-matcher/providers/document-aggregator.ts +12 -11
  66. package/src/modules/charges-matcher/providers/match-scorer.provider.ts +97 -17
  67. package/src/modules/charges-matcher/providers/single-match.provider.ts +21 -8
  68. package/src/modules/charges-matcher/providers/transaction-aggregator.ts +1 -1
  69. 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:** Simplified from original spec which proposed different date selection per document type.
517
- Current implementation uses `event_date` for all cases, providing consistent and predictable
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 type { Document, DocumentType, Transaction } from '../types.js';
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
- 'INVOICE',
18
- 'CREDIT_INVOICE',
19
- 'RECEIPT',
20
- 'INVOICE_RECEIPT',
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
- * @returns Confidence score from 0.0 (30+ days) to 1.0 (same day)
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(date1: Date, date2: Date): number {
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 === 'CREDIT_INVOICE') {
62
+ if (documentType === DocumentType.CreditInvoice) {
73
63
  normalizedAmount = -normalizedAmount;
74
64
  }
75
65
 
@@ -13,5 +13,4 @@ export const chargesMatcherModule = createModule({
13
13
  providers: [ChargesMatcherProvider],
14
14
  });
15
15
 
16
- export { ChargesMatcherProvider } from './providers/charges-matcher.provider.js';
17
16
  export * as ChargesMatcherTypes from './types.js';
@@ -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
- ): ProcessChargeResult {
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(sourceCharge, candidates, adminBusinessId);
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 { currency } from '../../documents/types.js';
10
- import { normalizeDocumentAmount, type DocumentType } from '../helpers/document-amount.helper.js';
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: DocumentType;
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[] = ['INVOICE', 'CREDIT_INVOICE'];
37
- const RECEIPT_TYPES: DocumentType[] = ['RECEIPT', 'INVOICE_RECEIPT'];
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: 'INVOICE', ... },
81
- * { total_amount: 50, currency_code: 'USD', creditor_id: 'b1', debtor_id: 'u1', type: 'INVOICE', ... }
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 {