From 4628ff0e3b38c7f1ab5997a22fab9fee61ddc187 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 19:38:32 -0500 Subject: [PATCH 1/9] Add diviners and types --- .../payments/src/Amount/Iso4217Currency.ts | 557 ++++++++++++++++++ .../packages/payments/src/Amount/Payload.ts | 36 ++ .../packages/payments/src/Amount/index.ts | 2 + .../packages/payments/src/Discount/Config.ts | 34 ++ .../packages/payments/src/Discount/Diviner.ts | 158 +++++ .../packages/payments/src/Discount/Params.ts | 14 + .../Payload/Coupon/Coupons/FixedAmount.ts | 37 ++ .../Payload/Coupon/Coupons/FixedPercentage.ts | 37 ++ .../Discount/Payload/Coupon/Coupons/index.ts | 2 + .../src/Discount/Payload/Coupon/Payload.ts | 25 + .../src/Discount/Payload/Coupon/Schema.ts | 2 + .../src/Discount/Payload/Coupon/index.ts | 4 + .../Payload/Coupon/types/CouponFields.ts | 11 + .../Discount/Payload/Coupon/types/index.ts | 2 + .../Payload/Coupon/types/isStackable.ts | 6 + .../payments/src/Discount/Payload/Discount.ts | 33 ++ .../src/Discount/Payload/NoDiscount.ts | 8 + .../payments/src/Discount/Payload/index.ts | 3 + .../packages/payments/src/Discount/index.ts | 5 + .../payments/src/Discount/lib/applyCoupons.ts | 59 ++ .../payments/src/Discount/lib/index.ts | 1 + .../Discount/lib/spec/applyCoupons.spec.ts | 86 +++ .../src/Discount/lib/spec/tsconfig.json | 3 + .../src/Discount/spec/Diviner.spec.ts | 127 ++++ .../payments/src/Discount/spec/tsconfig.json | 3 + .../packages/payments/src/Invoice/Invoice.ts | 20 + .../src/Invoice/getInvoiceForEscrow.ts | 43 ++ .../packages/payments/src/Invoice/index.ts | 2 + .../Invoice/spec/getInvoiceForEscrow.spec.ts | 69 +++ .../payments/src/Invoice/spec/tsconfig.json | 3 + .../packages/payments/src/Subtotal/Config.ts | 13 + .../packages/payments/src/Subtotal/Diviner.ts | 66 +++ .../packages/payments/src/Subtotal/Params.ts | 8 + .../packages/payments/src/Subtotal/Payload.ts | 33 ++ .../packages/payments/src/Subtotal/index.ts | 4 + .../src/Subtotal/lib/appraisalValidators.ts | 45 ++ .../src/Subtotal/lib/durationValidators.ts | 18 + .../payments/src/Subtotal/lib/index.ts | 2 + .../src/Subtotal/lib/termsValidators.ts | 18 + .../src/Subtotal/spec/Diviner.spec.ts | 110 ++++ .../payments/src/Subtotal/spec/tsconfig.json | 3 + .../packages/payments/src/Total/Config.ts | 24 + .../packages/payments/src/Total/Diviner.ts | 70 +++ .../packages/payments/src/Total/Params.ts | 8 + .../packages/payments/src/Total/Payload.ts | 33 ++ .../packages/payments/src/Total/index.ts | 4 + .../payments/src/Total/spec/Diviner.spec.ts | 173 ++++++ .../payments/src/Total/spec/tsconfig.json | 3 + .../payload/packages/payments/src/index.ts | 5 + .../payloadset/packages/payments/.depcheckrc | 3 + .../payloadset/packages/payments/.npmignore | 21 + packages/payloadset/packages/payments/LICENSE | 165 ++++++ .../payloadset/packages/payments/README.md | 13 + .../payloadset/packages/payments/package.json | 54 ++ .../src/Checkout/Amount/Iso4217Currency.ts | 557 ++++++++++++++++++ .../payments/src/Checkout/Amount/Payload.ts | 36 ++ .../payments/src/Checkout/Amount/index.ts | 2 + .../payments/src/Checkout/Discount/Config.ts | 34 ++ .../payments/src/Checkout/Discount/Diviner.ts | 158 +++++ .../payments/src/Checkout/Discount/Params.ts | 14 + .../Payload/Coupon/Coupons/FixedAmount.ts | 37 ++ .../Payload/Coupon/Coupons/FixedPercentage.ts | 37 ++ .../Discount/Payload/Coupon/Coupons/index.ts | 2 + .../Discount/Payload/Coupon/Payload.ts | 25 + .../Discount/Payload/Coupon/Schema.ts | 2 + .../Checkout/Discount/Payload/Coupon/index.ts | 4 + .../Payload/Coupon/types/CouponFields.ts | 11 + .../Discount/Payload/Coupon/types/index.ts | 2 + .../Payload/Coupon/types/isStackable.ts | 6 + .../src/Checkout/Discount/Payload/Discount.ts | 33 ++ .../Checkout/Discount/Payload/NoDiscount.ts | 8 + .../src/Checkout/Discount/Payload/index.ts | 3 + .../payments/src/Checkout/Discount/index.ts | 5 + .../src/Checkout/Discount/lib/applyCoupons.ts | 59 ++ .../src/Checkout/Discount/lib/index.ts | 1 + .../Discount/lib/spec/applyCoupons.spec.ts | 86 +++ .../Checkout/Discount/lib/spec/tsconfig.json | 3 + .../Checkout/Discount/spec/Diviner.spec.ts | 127 ++++ .../src/Checkout/Discount/spec/tsconfig.json | 3 + .../payments/src/Checkout/Invoice/Invoice.ts | 20 + .../Checkout/Invoice/getInvoiceForEscrow.ts | 43 ++ .../payments/src/Checkout/Invoice/index.ts | 2 + .../Invoice/spec/getInvoiceForEscrow.spec.ts | 69 +++ .../src/Checkout/Invoice/spec/tsconfig.json | 3 + .../payments/src/Checkout/Subtotal/Config.ts | 13 + .../payments/src/Checkout/Subtotal/Diviner.ts | 66 +++ .../payments/src/Checkout/Subtotal/Params.ts | 8 + .../payments/src/Checkout/Subtotal/Payload.ts | 33 ++ .../payments/src/Checkout/Subtotal/index.ts | 4 + .../Subtotal/lib/appraisalValidators.ts | 45 ++ .../Subtotal/lib/durationValidators.ts | 18 + .../src/Checkout/Subtotal/lib/index.ts | 2 + .../Checkout/Subtotal/lib/termsValidators.ts | 18 + .../Checkout/Subtotal/spec/Diviner.spec.ts | 110 ++++ .../src/Checkout/Subtotal/spec/tsconfig.json | 3 + .../payments/src/Checkout/Total/Config.ts | 24 + .../payments/src/Checkout/Total/Diviner.ts | 70 +++ .../payments/src/Checkout/Total/Params.ts | 8 + .../payments/src/Checkout/Total/Payload.ts | 33 ++ .../payments/src/Checkout/Total/index.ts | 4 + .../src/Checkout/Total/spec/Diviner.spec.ts | 173 ++++++ .../src/Checkout/Total/spec/tsconfig.json | 3 + .../packages/payments/src/Checkout/index.ts | 5 + .../payloadset/packages/payments/src/index.ts | 0 .../packages/payments/tsconfig.json | 4 + .../packages/payments/tsconfig.typedoc.json | 5 + .../payloadset/packages/payments/typedoc.json | 5 + .../payloadset/packages/payments/xy.config.ts | 11 + 108 files changed, 4345 insertions(+) create mode 100644 packages/payload/packages/payments/src/Amount/Iso4217Currency.ts create mode 100644 packages/payload/packages/payments/src/Amount/Payload.ts create mode 100644 packages/payload/packages/payments/src/Amount/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/Config.ts create mode 100644 packages/payload/packages/payments/src/Discount/Diviner.ts create mode 100644 packages/payload/packages/payments/src/Discount/Params.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedAmount.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedPercentage.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/Payload.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/Schema.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/types/CouponFields.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Coupon/types/isStackable.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/Discount.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/NoDiscount.ts create mode 100644 packages/payload/packages/payments/src/Discount/Payload/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts create mode 100644 packages/payload/packages/payments/src/Discount/lib/index.ts create mode 100644 packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts create mode 100644 packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json create mode 100644 packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts create mode 100644 packages/payload/packages/payments/src/Discount/spec/tsconfig.json create mode 100644 packages/payload/packages/payments/src/Invoice/Invoice.ts create mode 100644 packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts create mode 100644 packages/payload/packages/payments/src/Invoice/index.ts create mode 100644 packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts create mode 100644 packages/payload/packages/payments/src/Invoice/spec/tsconfig.json create mode 100644 packages/payload/packages/payments/src/Subtotal/Config.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/Diviner.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/Params.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/Payload.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/index.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/lib/index.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts create mode 100644 packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json create mode 100644 packages/payload/packages/payments/src/Total/Config.ts create mode 100644 packages/payload/packages/payments/src/Total/Diviner.ts create mode 100644 packages/payload/packages/payments/src/Total/Params.ts create mode 100644 packages/payload/packages/payments/src/Total/Payload.ts create mode 100644 packages/payload/packages/payments/src/Total/index.ts create mode 100644 packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts create mode 100644 packages/payload/packages/payments/src/Total/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/.depcheckrc create mode 100644 packages/payloadset/packages/payments/.npmignore create mode 100644 packages/payloadset/packages/payments/LICENSE create mode 100644 packages/payloadset/packages/payments/README.md create mode 100644 packages/payloadset/packages/payments/package.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Config.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Params.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/index.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts create mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json create mode 100644 packages/payloadset/packages/payments/src/Checkout/index.ts create mode 100644 packages/payloadset/packages/payments/src/index.ts create mode 100644 packages/payloadset/packages/payments/tsconfig.json create mode 100644 packages/payloadset/packages/payments/tsconfig.typedoc.json create mode 100644 packages/payloadset/packages/payments/typedoc.json create mode 100644 packages/payloadset/packages/payments/xy.config.ts diff --git a/packages/payload/packages/payments/src/Amount/Iso4217Currency.ts b/packages/payload/packages/payments/src/Amount/Iso4217Currency.ts new file mode 100644 index 000000000..2344e0398 --- /dev/null +++ b/packages/payload/packages/payments/src/Amount/Iso4217Currency.ts @@ -0,0 +1,557 @@ +/* eslint-disable max-lines */ +/** + * ISO 4217 currency codes + */ +export type Iso4217AlphabeticalCode = + 'AED' | + 'AFN' | + 'ALL' | + 'AMD' | + 'ANG' | + 'AOA' | + 'ARS' | + 'AUD' | + 'AWG' | + 'AZN' | + 'BAM' | + 'BBD' | + 'BDT' | + 'BGN' | + 'BHD' | + 'BIF' | + 'BMD' | + 'BND' | + 'BOB' | + 'BOV' | + 'BRL' | + 'BSD' | + 'BTN' | + 'BWP' | + 'BYN' | + 'BZD' | + 'CAD' | + 'CDF' | + 'CHE' | + 'CHF' | + 'CHW' | + 'CLF' | + 'CLP' | + 'CNY' | + 'COP' | + 'COU' | + 'CRC' | + 'CUP' | + 'CVE' | + 'CZK' | + 'DJF' | + 'DKK' | + 'DOP' | + 'DZD' | + 'EGP' | + 'ERN' | + 'ETB' | + 'EUR' | + 'FJD' | + 'FKP' | + 'GBP' | + 'GEL' | + 'GHS' | + 'GIP' | + 'GMD' | + 'GNF' | + 'GTQ' | + 'GYD' | + 'HKD' | + 'HNL' | + 'HTG' | + 'HUF' | + 'IDR' | + 'ILS' | + 'INR' | + 'IQD' | + 'IRR' | + 'ISK' | + 'JMD' | + 'JOD' | + 'JPY' | + 'KES' | + 'KGS' | + 'KHR' | + 'KMF' | + 'KPW' | + 'KRW' | + 'KWD' | + 'KYD' | + 'KZT' | + 'LAK' | + 'LBP' | + 'LKR' | + 'LRD' | + 'LSL' | + 'LYD' | + 'MAD' | + 'MDL' | + 'MGA' | + 'MKD' | + 'MMK' | + 'MNT' | + 'MOP' | + 'MRU' | + 'MUR' | + 'MVR' | + 'MWK' | + 'MXN' | + 'MXV' | + 'MYR' | + 'MZN' | + 'NAD' | + 'NGN' | + 'NIO' | + 'NOK' | + 'NPR' | + 'NZD' | + 'OMR' | + 'PAB' | + 'PEN' | + 'PGK' | + 'PHP' | + 'PKR' | + 'PLN' | + 'PYG' | + 'QAR' | + 'RON' | + 'RSD' | + 'RUB' | + 'RWF' | + 'SAR' | + 'SBD' | + 'SCR' | + 'SDG' | + 'SEK' | + 'SGD' | + 'SHP' | + 'SLE' | + 'SOS' | + 'SRD' | + 'SSP' | + 'STN' | + 'SVC' | + 'SYP' | + 'SZL' | + 'THB' | + 'TJS' | + 'TMT' | + 'TND' | + 'TOP' | + 'TRY' | + 'TTD' | + 'TWD' | + 'TZS' | + 'UAH' | + 'UGX' | + 'USD' | + 'USN' | + 'UYI' | + 'UYU' | + 'UYW' | + 'UZS' | + 'VED' | + 'VES' | + 'VND' | + 'VUV' | + 'WST' | + 'XAF' | + 'XAG' | + 'XAU' | + 'XBA' | + 'XBB' | + 'XBC' | + 'XBD' | + 'XCD' | + 'XDR' | + 'XOF' | + 'XPD' | + 'XPF' | + 'XPT' | + 'XSU' | + 'XTS' | + 'XUA' | + 'XXX' | + 'YER' | + 'ZAR' | + 'ZMW' | + 'ZWG' | + 'ZWL' + +// TODO: Technically, the values should be 3 digit numbers with leading +// zeros, so we can padStart if we need to be strict +/** + * ISO 4217 numeric currency number codes + */ +export type Iso4217NumericCode = + 784 | + 971 | + 8 | + 51 | + 532 | + 973 | + 32 | + 36 | + 533 | + 944 | + 977 | + 52 | + 50 | + 975 | + 48 | + 108 | + 60 | + 96 | + 68 | + 984 | + 986 | + 44 | + 64 | + 72 | + 933 | + 84 | + 124 | + 976 | + 947 | + 756 | + 948 | + 990 | + 152 | + 156 | + 170 | + 970 | + 188 | + 192 | + 132 | + 203 | + 262 | + 208 | + 214 | + 12 | + 818 | + 232 | + 230 | + 978 | + 242 | + 238 | + 826 | + 981 | + 936 | + 292 | + 270 | + 324 | + 320 | + 328 | + 344 | + 340 | + 332 | + 348 | + 360 | + 376 | + 356 | + 368 | + 364 | + 352 | + 388 | + 400 | + 392 | + 404 | + 417 | + 116 | + 174 | + 408 | + 410 | + 414 | + 136 | + 398 | + 418 | + 422 | + 144 | + 430 | + 426 | + 434 | + 504 | + 498 | + 969 | + 807 | + 104 | + 496 | + 446 | + 929 | + 480 | + 462 | + 454 | + 484 | + 979 | + 458 | + 943 | + 516 | + 566 | + 558 | + 578 | + 524 | + 554 | + 512 | + 590 | + 604 | + 598 | + 608 | + 586 | + 985 | + 600 | + 634 | + 946 | + 941 | + 643 | + 646 | + 682 | + 90 | + 690 | + 938 | + 752 | + 702 | + 654 | + 925 | + 706 | + 968 | + 728 | + 930 | + 222 | + 760 | + 748 | + 764 | + 972 | + 934 | + 788 | + 776 | + 949 | + 780 | + 901 | + 834 | + 980 | + 800 | + 840 | + 997 | + 940 | + 858 | + 927 | + 860 | + 926 | + 928 | + 704 | + 548 | + 882 | + 950 | + 961 | + 959 | + 955 | + 956 | + 957 | + 958 | + 951 | + 960 | + 952 | + 964 | + 953 | + 962 | + 994 | + 963 | + 965 | + 999 | + 886 | + 710 | + 967 | + 924 | + 932 + +/** + * Dictionary of ISO 4217 alphabetical currency codes to numeric currency number codes + */ +export const Iso4217CurrencyCodes: Record = { + AED: 784, + AFN: 971, + ALL: 8, + AMD: 51, + ANG: 532, + AOA: 973, + ARS: 32, + AUD: 36, + AWG: 533, + AZN: 944, + BAM: 977, + BBD: 52, + BDT: 50, + BGN: 975, + BHD: 48, + BIF: 108, + BMD: 60, + BND: 96, + BOB: 68, + BOV: 984, + BRL: 986, + BSD: 44, + BTN: 64, + BWP: 72, + BYN: 933, + BZD: 84, + CAD: 124, + CDF: 976, + CHE: 947, + CHF: 756, + CHW: 948, + CLF: 990, + CLP: 152, + CNY: 156, + COP: 170, + COU: 970, + CRC: 188, + CUP: 192, + CVE: 132, + CZK: 203, + DJF: 262, + DKK: 208, + DOP: 214, + DZD: 12, + EGP: 818, + ERN: 232, + ETB: 230, + EUR: 978, + FJD: 242, + FKP: 238, + GBP: 826, + GEL: 981, + GHS: 936, + GIP: 292, + GMD: 270, + GNF: 324, + GTQ: 320, + GYD: 328, + HKD: 344, + HNL: 340, + HTG: 332, + HUF: 348, + IDR: 360, + ILS: 376, + INR: 356, + IQD: 368, + IRR: 364, + ISK: 352, + JMD: 388, + JOD: 400, + JPY: 392, + KES: 404, + KGS: 417, + KHR: 116, + KMF: 174, + KPW: 408, + KRW: 410, + KWD: 414, + KYD: 136, + KZT: 398, + LAK: 418, + LBP: 422, + LKR: 144, + LRD: 430, + LSL: 426, + LYD: 434, + MAD: 504, + MDL: 498, + MGA: 969, + MKD: 807, + MMK: 104, + MNT: 496, + MOP: 446, + MRU: 929, + MUR: 480, + MVR: 462, + MWK: 454, + MXN: 484, + MXV: 979, + MYR: 458, + MZN: 943, + NAD: 516, + NGN: 566, + NIO: 558, + NOK: 578, + NPR: 524, + NZD: 554, + OMR: 512, + PAB: 590, + PEN: 604, + PGK: 598, + PHP: 608, + PKR: 586, + PLN: 985, + PYG: 600, + QAR: 634, + RON: 946, + RSD: 941, + RUB: 643, + RWF: 646, + SAR: 682, + SBD: 90, + SCR: 690, + SDG: 938, + SEK: 752, + SGD: 702, + SHP: 654, + SLE: 925, + SOS: 706, + SRD: 968, + SSP: 728, + STN: 930, + SVC: 222, + SYP: 760, + SZL: 748, + THB: 764, + TJS: 972, + TMT: 934, + TND: 788, + TOP: 776, + TRY: 949, + TTD: 780, + TWD: 901, + TZS: 834, + UAH: 980, + UGX: 800, + USD: 840, + USN: 997, + UYI: 940, + UYU: 858, + UYW: 927, + UZS: 860, + VED: 926, + VES: 928, + VND: 704, + VUV: 548, + WST: 882, + XAF: 950, + XAG: 961, + XAU: 959, + XBA: 955, + XBB: 956, + XBC: 957, + XBD: 958, + XCD: 951, + XDR: 960, + XOF: 952, + XPD: 964, + XPF: 953, + XPT: 962, + XSU: 994, + XTS: 963, + XUA: 965, + XXX: 999, + YER: 886, + ZAR: 710, + ZMW: 967, + ZWG: 924, + ZWL: 932, +} + +export const isIso4217CurrencyCode = (code: string): code is Iso4217AlphabeticalCode => Iso4217CurrencyCodes[code as Iso4217AlphabeticalCode] ? true : false diff --git a/packages/payload/packages/payments/src/Amount/Payload.ts b/packages/payload/packages/payments/src/Amount/Payload.ts new file mode 100644 index 000000000..bfd293c60 --- /dev/null +++ b/packages/payload/packages/payments/src/Amount/Payload.ts @@ -0,0 +1,36 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { Iso4217AlphabeticalCode } from './Iso4217Currency.ts' + +export const AmountSchema = 'network.xyo.payments.amount' as const +export type AmountSchema = typeof AmountSchema + +export interface AmountFields { + amount: number + currency: Iso4217AlphabeticalCode +} + +/** + * The result of a amount + */ +export type Amount = PayloadWithSources + +/** + * Identity function for determining if an object is an Amount + */ +export const isAmount = isPayloadOfSchemaType(AmountSchema) + +/** + * Identity function for determining if an object is an Amount with sources + */ +export const isAmountWithSources = isPayloadOfSchemaTypeWithSources(AmountSchema) + +/** + * Identity function for determining if an object is an Amount with meta + */ +export const isAmountWithMeta = isPayloadOfSchemaTypeWithMeta(AmountSchema) diff --git a/packages/payload/packages/payments/src/Amount/index.ts b/packages/payload/packages/payments/src/Amount/index.ts new file mode 100644 index 000000000..155f9afe3 --- /dev/null +++ b/packages/payload/packages/payments/src/Amount/index.ts @@ -0,0 +1,2 @@ +export * from './Iso4217Currency.ts' +export * from './Payload.ts' diff --git a/packages/payload/packages/payments/src/Discount/Config.ts b/packages/payload/packages/payments/src/Discount/Config.ts new file mode 100644 index 000000000..842b2c64c --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Config.ts @@ -0,0 +1,34 @@ +// import type { Hash } from '@xylabs/hex' +import type { Address } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' +import type { ModuleIdentifier } from '@xyo-network/module-model' + +export const PaymentDiscountDivinerConfigSchema = 'network.xyo.diviner.payments.discount.config' +export type PaymentDiscountDivinerConfigSchema = typeof PaymentDiscountDivinerConfigSchema + +/** + * The configuration for the Payment Discount Diviner + */ +export type PaymentDiscountDivinerConfig = DivinerConfig< + { + /** + * The boundwitness diviner used to query for payloads + */ + boundWitnessDiviner?: ModuleIdentifier + /** + * The list of coupon authorities that can be used to get a discount + */ + couponAuthorities?: Address[] + + // /** + // * The list of coupons that are supported by this diviner + // */ + // supportedCoupons?: Hash[] + + /** + * The Diviner that can be used to determine the subtotal to apply discounts to + */ + paymentSubtotalDiviner?: ModuleIdentifier + }, + PaymentDiscountDivinerConfigSchema +> diff --git a/packages/payload/packages/payments/src/Discount/Diviner.ts b/packages/payload/packages/payments/src/Discount/Diviner.ts new file mode 100644 index 000000000..aeaedc751 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Diviner.ts @@ -0,0 +1,158 @@ +import { assertEx } from '@xylabs/assert' +import { exists } from '@xylabs/exists' +import { Address, Hash } from '@xylabs/hex' +import { ArchivistInstance, asArchivistInstance } from '@xyo-network/archivist-model' +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model' +import { + HashLeaseEstimate, + isHashLeaseEstimate, +} from '@xyo-network/diviner-hash-lease' +import { + asDivinerInstance, DivinerInstance, DivinerModuleEventData, +} from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import { Payload } from '@xyo-network/payload-model' +import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { PaymentDiscountDivinerConfigSchema } from './Config.ts' +import { applyCoupons } from './lib/index.ts' +import { PaymentDiscountDivinerParams } from './Params.ts' +import { + Coupon, + Discount, + isCoupon, + isCouponWithMeta, + NO_DISCOUNT, +} from './Payload/index.ts' + +const DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly = { + limit: 1, + order: 'desc', + schema: BoundWitnessDivinerQuerySchema, +} + +export type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload + +@creatableModule() +export class PaymentDiscountDiviner< + TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams, + TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType, + TOut extends Discount = Discount, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentDiscountDivinerConfigSchema] + static override defaultConfigSchema = PaymentDiscountDivinerConfigSchema + + protected get couponAuthorities(): Address[] { + return [...(this.config.couponAuthorities ?? []), ...(this.params.couponAuthorities ?? [])] + } + + protected async divineHandler(payloads: TIn[] = []): Promise { + const sources: Hash[] = [] + + // Parse terms + const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined + if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[] + sources.push(await PayloadBuilder.hash(terms)) + + // Parse discounts + const discountHashes = terms.discounts ?? [] + if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + // TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic + // Parse appraisals + const termsAppraisals = terms?.appraisals + if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + const hashMap = await PayloadBuilder.toAllHashMap(payloads) + const foundAppraisals = termsAppraisals.filter(hash => hashMap[hash]) + // Add the appraisals that were found to the sources + sources.push(...foundAppraisals) + // If not all appraisals are found, return no discount + if (foundAppraisals.length !== termsAppraisals.length) { + return [{ ...NO_DISCOUNT, sources }] as TOut[] + } + // TODO: Cast should not be required + const appraisals = foundAppraisals.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] + + // Use the supplied payloads to find the discounts + const discounts = discountHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) as Coupon[] + // Find any remaining coupons from the archivist + if (discounts.length !== discountHashes.length) { + // Find remaining from discounts archivist + const discountsArchivist = await this.getDiscountsArchivist() + const foundDiscounts = await discountsArchivist.get(discountHashes) + discounts.push(...foundDiscounts.filter(isCouponWithMeta)) + } + const discountsMap = await PayloadBuilder.toAllHashMap(discounts) + if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + // Add the found discounts to the sources + const foundDiscountsHashes = Object.keys(discountsMap) as Hash[] + sources.push(...foundDiscountsHashes) + + // Log individual discounts that were not found + for (const hash of discountHashes) { + if (!foundDiscountsHashes.includes(hash)) { + console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`) + } + } + + // Parse coupons + const coupons = Object.values(discountsMap) + const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent)) + if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + const discount = applyCoupons(appraisals, validCoupons) + return [{ ...discount, sources }] as TOut[] + } + + /** + * Filters the supplied list of coupons to only those that are signed by + * addresses specified in the couponAuthorities + * @param coupons The list of coupons to filter + * @returns The filtered list of coupons that are signed by the couponAuthorities + */ + protected async filterToSigned(coupons: Coupon[]): Promise { + const signed: Coupon[] = [] + const dataHashMap = await PayloadBuilder.toDataHashMap(coupons) + const boundWitnessDiviner = await this.getDiscountsBoundWitnessDiviner() + const hashes = Object.keys(dataHashMap) + const addresses = this.couponAuthorities + // TODO: Keep an in memory cache of the hashes queried and their results + // to avoid querying the same hash multiple times + await Promise.all(hashes.map((h) => { + const hash = h as Hash + return Promise.all(addresses.map(async (address) => { + const query: BoundWitnessDivinerQueryPayload = { + ...DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS, addresses: [address], payload_hashes: [hash], + } + const result = await boundWitnessDiviner.divine([query]) + if (result.length > 0) signed.push(dataHashMap[hash]) + })) + })) + return signed + } + + protected async getDiscountsArchivist(): Promise { + const name = assertEx(this.config.archivist, () => 'Missing archivist in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving archivist: ${name}`) + return assertEx(asArchivistInstance(mod), () => `Resolved module ${mod.address} not a valid Archivist`) + } + + protected async getDiscountsBoundWitnessDiviner(): Promise { + const name = assertEx(this.config.boundWitnessDiviner, () => 'Missing boundWitnessDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) + } + + protected isCouponCurrent(coupon: Coupon): boolean { + const now = Date.now() + return coupon.exp > now && coupon.nbf < now + } +} diff --git a/packages/payload/packages/payments/src/Discount/Params.ts b/packages/payload/packages/payments/src/Discount/Params.ts new file mode 100644 index 000000000..8ffb0774e --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Params.ts @@ -0,0 +1,14 @@ +import type { Address } from '@xylabs/hex' +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentDiscountDivinerConfig } from './Config.ts' + +export type PaymentDiscountDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedAmount.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedAmount.ts new file mode 100644 index 000000000..e8ea3e731 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedAmount.ts @@ -0,0 +1,37 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../../../../Amount/index.ts' +import { CouponSchema } from '../Schema.ts' +import type { CouponFields } from '../types/index.ts' + +export const FixedAmountCouponSchema = `${CouponSchema}.fixed.amount` as const +export type FixedAmountCouponSchema = typeof FixedAmountCouponSchema + +export interface FixedAmountCouponFields extends CouponFields, AmountFields { + +} + +/** + * A coupon that provides a fixed discount amount + */ +export type FixedAmountCoupon = PayloadWithSources + +/** + * Identity function for determining if an object is an FixedAmountCoupon + */ +export const isFixedAmountCoupon = isPayloadOfSchemaType(FixedAmountCouponSchema) + +/** + * Identity function for determining if an object is an FixedAmountCoupon with sources + */ +export const isFixedAmountCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedAmountCouponSchema) + +/** + * Identity function for determining if an object is an FixedAmountCoupon with meta + */ +export const isFixedAmountCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedAmountCouponSchema) diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedPercentage.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedPercentage.ts new file mode 100644 index 000000000..1d6942e69 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/FixedPercentage.ts @@ -0,0 +1,37 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import { CouponSchema } from '../Schema.ts' +import type { CouponFields } from '../types/index.ts' + +export const FixedPercentageCouponSchema = `${CouponSchema}.fixed.percentage` as const +export type FixedPercentageCouponSchema = typeof FixedPercentageCouponSchema + +export interface FixedPercentageCouponFields extends CouponFields { + percentage: number + +} + +/** + * A coupon that provides a fixed discount amount + */ +export type FixedPercentageCoupon = PayloadWithSources + +/** + * Identity function for determining if an object is an FixedPercentageCoupon + */ +export const isFixedPercentageCoupon = isPayloadOfSchemaType(FixedPercentageCouponSchema) + +/** + * Identity function for determining if an object is an FixedPercentageCoupon with sources + */ +export const isFixedPercentageCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedPercentageCouponSchema) + +/** + * Identity function for determining if an object is an FixedPercentageCoupon with meta + */ +export const isFixedPercentageCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedPercentageCouponSchema) diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/index.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/index.ts new file mode 100644 index 000000000..ff31c45af --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Coupons/index.ts @@ -0,0 +1,2 @@ +export * from './FixedAmount.ts' +export * from './FixedPercentage.ts' diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/Payload.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Payload.ts new file mode 100644 index 000000000..40bae5a2a --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Payload.ts @@ -0,0 +1,25 @@ +import { + type FixedAmountCoupon, type FixedPercentageCoupon, isFixedAmountCoupon, isFixedAmountCouponWithMeta, isFixedAmountCouponWithSources, isFixedPercentageCoupon, + isFixedPercentageCouponWithMeta, + isFixedPercentageCouponWithSources, +} from './Coupons/index.ts' + +/** + * The result of a discount + */ +export type Coupon = FixedAmountCoupon | FixedPercentageCoupon + +/** + * Identity function for determining if an object is an Coupon + */ +export const isCoupon = (x?: unknown | null) => isFixedAmountCoupon(x) || isFixedPercentageCoupon(x) + +/** + * Identity function for determining if an object is an Coupon with sources + */ +export const isCouponWithSources = (x?: unknown | null) => isFixedAmountCouponWithSources(x) || isFixedPercentageCouponWithSources(x) + +/** + * Identity function for determining if an object is an Coupon with meta + */ +export const isCouponWithMeta = (x?: unknown | null) => isFixedAmountCouponWithMeta(x) || isFixedPercentageCouponWithMeta(x) diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/Schema.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Schema.ts new file mode 100644 index 000000000..4cf0542af --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/Schema.ts @@ -0,0 +1,2 @@ +export const CouponSchema = 'network.xyo.payments.coupon' as const +export type CouponSchema = typeof CouponSchema diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/index.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/index.ts new file mode 100644 index 000000000..02c47de6f --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/index.ts @@ -0,0 +1,4 @@ +export * from './Coupons/index.ts' +export * from './Payload.ts' +export * from './Schema.ts' +export * from './types/index.ts' diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/CouponFields.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/CouponFields.ts new file mode 100644 index 000000000..18e472798 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/CouponFields.ts @@ -0,0 +1,11 @@ +import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' + +/** + * The fields that are common across all coupons + */ +export interface CouponFields extends DurationFields { + /** + * Whether or not this discount can be stacked with other discounts + */ + stackable?: boolean +} diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts new file mode 100644 index 000000000..ee5290191 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/index.ts @@ -0,0 +1,2 @@ +export * from './CouponFields.ts' +export * from './isStackable.ts' diff --git a/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/isStackable.ts b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/isStackable.ts new file mode 100644 index 000000000..a0fb58e29 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Coupon/types/isStackable.ts @@ -0,0 +1,6 @@ +import type { CouponFields } from './CouponFields.ts' + +/** + * Identity function for determining if coupon is stackable + */ +export const isStackable = (x?: unknown | null) => (x as CouponFields ?? { stackable: false })?.stackable diff --git a/packages/payload/packages/payments/src/Discount/Payload/Discount.ts b/packages/payload/packages/payments/src/Discount/Payload/Discount.ts new file mode 100644 index 000000000..f5fab74f8 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/Discount.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../../Amount/index.ts' + +export const DiscountSchema = 'network.xyo.payments.discount' as const +export type DiscountSchema = typeof DiscountSchema + +export interface DiscountFields extends AmountFields { } + +/** + * The result of a discount + */ +export type Discount = PayloadWithSources + +/** + * Identity function for determining if an object is an Discount + */ +export const isDiscount = isPayloadOfSchemaType(DiscountSchema) + +/** + * Identity function for determining if an object is an Discount with sources + */ +export const isDiscountWithSources = isPayloadOfSchemaTypeWithSources(DiscountSchema) + +/** + * Identity function for determining if an object is an Discount with meta + */ +export const isDiscountWithMeta = isPayloadOfSchemaTypeWithMeta(DiscountSchema) diff --git a/packages/payload/packages/payments/src/Discount/Payload/NoDiscount.ts b/packages/payload/packages/payments/src/Discount/Payload/NoDiscount.ts new file mode 100644 index 000000000..8075f78a7 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/NoDiscount.ts @@ -0,0 +1,8 @@ +import type { Discount } from './Discount.ts' +import { DiscountSchema } from './Discount.ts' + +export const NO_DISCOUNT: Discount = { + schema: DiscountSchema, + amount: 0, + currency: 'USD', +} diff --git a/packages/payload/packages/payments/src/Discount/Payload/index.ts b/packages/payload/packages/payments/src/Discount/Payload/index.ts new file mode 100644 index 000000000..a39b81c5c --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/Payload/index.ts @@ -0,0 +1,3 @@ +export * from './Coupon/index.ts' +export * from './Discount.ts' +export * from './NoDiscount.ts' diff --git a/packages/payload/packages/payments/src/Discount/index.ts b/packages/payload/packages/payments/src/Discount/index.ts new file mode 100644 index 000000000..6ee3fc3b0 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/index.ts @@ -0,0 +1,5 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './lib/index.ts' +export * from './Params.ts' +export * from './Payload/index.ts' diff --git a/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts b/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts new file mode 100644 index 000000000..a079c7ea5 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts @@ -0,0 +1,59 @@ +import { assertEx } from '@xylabs/assert' +import { exists } from '@xylabs/exists' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' + +import type { AmountFields } from '../../Amount/index.ts' +import type { + Coupon, Discount, FixedAmountCoupon, + FixedPercentageCoupon, +} from '../Payload/index.ts' +import { + DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon, + isStackable, +} from '../Payload/index.ts' + +export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => { + // Ensure all appraisals and coupons are in USD + const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD') + assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD') + const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial)?.currency).filter(exists).every(currency => currency === 'USD') + assertEx(allCouponsAreUSD, 'All coupons must be in USD') + const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0) + + // Calculated non-stackable discount coupons + const singularFixedDiscount = Math.max(...coupons + .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon)) + .map(coupon => (coupon as FixedAmountCoupon).amount), 0) + const singularPercentageDiscount = (Math.max(...coupons + .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon)) + .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total + + // Calculate stackable discount coupons + // First calculate the total discount from fixed amount coupons + const stackedFixedDiscount = coupons + .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon)) + .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0) + // Then calculate the total discount from percentage coupons and apply + // the percentage discount to the remaining total after fixed discounts + const stackedPercentageDiscount = coupons + .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon)) + .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount) + // Sum all stackable discounts + const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount + + // Find the best coupon(s) to apply + const maxDiscount = Math.max( + singularFixedDiscount, + singularPercentageDiscount, + stackedDiscount, + 0, + ) + + // Ensure discount is not more than the total + const amount = Math.min(maxDiscount, total) + + // Return single discount payload + return { + amount, schema: DiscountSchema, currency: 'USD', + } +} diff --git a/packages/payload/packages/payments/src/Discount/lib/index.ts b/packages/payload/packages/payments/src/Discount/lib/index.ts new file mode 100644 index 000000000..89c610e91 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/lib/index.ts @@ -0,0 +1 @@ +export * from './applyCoupons.ts' diff --git a/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts b/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts new file mode 100644 index 000000000..5a40c6a14 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts @@ -0,0 +1,86 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { + beforeEach, describe, it, vi, +} from 'vitest' + +import type { Coupon } from '../../Payload/index.ts' +import { + DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, +} from '../../Payload/index.ts' +import { applyCoupons } from '../applyCoupons.ts' + +describe('applyCoupons', () => { + const nbf = Date.now() + const exp = Date.now() + 10_000_000 + // Coupons + const TEN_DOLLAR_OFF_COUPON: Coupon = { + amount: 10, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', + } + const TEN_PERCENT_OFF_COUPON: Coupon = { + percentage: 0.1, exp, nbf, schema: FixedPercentageCouponSchema, + } + // Appraisals + const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const SEVENTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 70, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const THIRTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 30, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + describe('when coupon is less than total', () => { + const validCoupons: [HashLeaseEstimate[], Coupon[]][] = [ + [[HUNDRED_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], + [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], + [[HUNDRED_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], + [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], + ] + it.each(validCoupons)('Applies coupon discount', (estimates, coupons) => { + const results = applyCoupons(estimates, coupons) + expect(results).toEqual({ + amount: 10, schema: DiscountSchema, currency: 'USD', + }) + }) + }) + describe('when discount exceeds total', () => { + const discountExceedsTotal: [HashLeaseEstimate[], Coupon[]][] = [ + [ + [HUNDRED_DOLLAR_ESTIMATE], + [{ + amount: 101, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', + }]], + [ + [HUNDRED_DOLLAR_ESTIMATE], + [{ + percentage: 1.1, exp, nbf, schema: FixedPercentageCouponSchema, + }]], + ] + it.each(discountExceedsTotal)('Discounts only to total', (estimates, coupons) => { + const results = applyCoupons(estimates, coupons) + const amount = estimates.reduce((acc, a) => acc + a.price, 0) + expect(results).toEqual({ + amount, schema: DiscountSchema, currency: 'USD', + }) + }) + }) + describe('with stackable discounts', () => { + const STACKABLE_TEN_DOLLAR_OFF_COUPON: Coupon = { ...TEN_DOLLAR_OFF_COUPON, stackable: true } + const STACKABLE_TEN_PERCENT_OFF_COUPON: Coupon = { ...TEN_PERCENT_OFF_COUPON, stackable: true } + const validCoupons: [HashLeaseEstimate[], Coupon[], number][] = [ + [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_DOLLAR_OFF_COUPON, STACKABLE_TEN_PERCENT_OFF_COUPON], 19], + [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_PERCENT_OFF_COUPON, STACKABLE_TEN_DOLLAR_OFF_COUPON], 19], + ] + it.each(validCoupons)('Applies both discounts', (estimates, coupons, amount) => { + const results = applyCoupons(estimates, coupons) + expect(results).toEqual({ + amount, schema: DiscountSchema, currency: 'USD', + }) + }) + }) +}) diff --git a/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json b/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts b/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts new file mode 100644 index 000000000..c7e32b7d3 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts @@ -0,0 +1,127 @@ +import { HDWallet } from '@xyo-network/account' +import { MemoryArchivist } from '@xyo-network/archivist-memory' +import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' +import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentDiscountDivinerConfigSchema } from '../Config.ts' +import { PaymentDiscountDiviner } from '../Diviner.ts' +import type { Coupon } from '../Payload/index.ts' +import { + DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, + isDiscount, +} from '../Payload/index.ts' + +describe('PaymentDiscountDiviner', () => { + let sut: PaymentDiscountDiviner + const nbf = Date.now() + const exp = Number.MAX_SAFE_INTEGER + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const validCoupons: Coupon[] = [ + { + amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, + }, + ] + const unsignedCoupons: Coupon[] = [ + { + amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, + }, + ] + beforeAll(async () => { + termsBase.appraisals?.push(await PayloadBuilder.hash(HUNDRED_DOLLAR_ESTIMATE)) + const node = await MemoryNode.create({ account: 'random' }) + const archivist = await MemoryArchivist.create({ account: 'random' }) + const signer = await HDWallet.random() + // Sign the valid coupons and insert them into the archivist + for (const coupon of validCoupons) { + const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() + await archivist.insert([bw, ...payloads]) + } + // Insert (but do not sign) the unsigned coupons into the archivist + await archivist.insert(unsignedCoupons) + const boundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ + account: 'random', + config: { + archivist: archivist.address, + schema: MemoryBoundWitnessDiviner.defaultConfigSchema, + }, + }) + sut = await PaymentDiscountDiviner.create({ + account: 'random', + config: { + archivist: archivist.address, + boundWitnessDiviner: boundWitnessDiviner.address, + couponAuthorities: [signer.address], + schema: PaymentDiscountDivinerConfigSchema, + }, + }) + const modules = [archivist, boundWitnessDiviner, sut] + for (const mod of modules) { + await node.register(mod) + await node.attach(mod.address, false) + } + }) + beforeEach(() => { + vi.clearAllMocks() + }) + describe('with valid coupon', () => { + it.each(validCoupons)('Applies coupon', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 10, schema: DiscountSchema }) + }) + }) + describe('with invalid coupon', () => { + it.each(unsignedCoupons)('Does not apply coupons', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) + }) + }) + describe('with expired coupon', () => { + const now = Date.now() + const expiredCoupons: Coupon[] = [ + // In past + { + amount: 10, exp: now, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + // In future + { + percentage: 0.1, exp: now + 100_000_000, nbf: now + 10_000_000, schema: FixedPercentageCouponSchema, + }, + ] + it.each(expiredCoupons)('Does not apply coupons', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) + }) + }) +}) diff --git a/packages/payload/packages/payments/src/Discount/spec/tsconfig.json b/packages/payload/packages/payments/src/Discount/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/Invoice/Invoice.ts b/packages/payload/packages/payments/src/Invoice/Invoice.ts new file mode 100644 index 000000000..42b7e9050 --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/Invoice.ts @@ -0,0 +1,20 @@ +import type { Payment } from '@xyo-network/payment-payload-plugins' + +import type { Discount } from '../Discount/index.ts' +import type { Subtotal } from '../Subtotal/index.ts' +import type { Total } from '../Total/index.ts' + +/** + * A tuple containing the subtotal, total, and payment for an invoice. + */ +export type StandardInvoice = [Subtotal, Total, Payment] + +/** + * A tuple containing the subtotal, total, payment, and discount for an invoice. + */ +export type DiscountedInvoice = [...StandardInvoice, Discount] + +/** + * An invoice. + */ +export type Invoice = StandardInvoice | DiscountedInvoice diff --git a/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts b/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts new file mode 100644 index 000000000..b4eba57bd --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts @@ -0,0 +1,43 @@ +import type { Hash } from '@xylabs/hex' +import type { DivinerInstance } from '@xyo-network/diviner-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { Payload } from '@xyo-network/payload-model' + +import type { Discount } from '../Discount/index.ts' +import { isDiscount } from '../Discount/index.ts' +import type { EscrowTerms } from '../Escrow/index.ts' +import { type Payment, PaymentSchema } from '../Payment/index.ts' +import type { Subtotal } from '../Subtotal/index.ts' +import { isSubtotal } from '../Subtotal/index.ts' +import type { Total } from '../Total/index.ts' +import { isTotal } from '../Total/index.ts' +import type { Invoice } from './Invoice.ts' + +/** + * Validates the escrow terms to ensure they are valid for a purchase + * @returns A payment if the terms are valid for a purchase, undefined otherwise + */ +export const getInvoiceForEscrow = async ( + terms: EscrowTerms, + dataHashMap: Record, + paymentTotalDiviner: DivinerInstance, +): Promise => { + const payloads = Object.values(dataHashMap) + const results = await paymentTotalDiviner.divine([terms, ...payloads]) + const subtotal = results.find(isSubtotal) as Subtotal | undefined + const discount = results.find(isDiscount) as Discount | undefined + const total = results.find(isTotal) as Total | undefined + if (!subtotal || !total) return undefined + const { amount, currency } = total + if (currency !== 'USD') return undefined + const sources = await getSources(terms, subtotal, total, discount) + const payment: Payment = { + amount, currency, schema: PaymentSchema, sources, + } + return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment] +} + +const getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise => { + const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total] + return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p))) +} diff --git a/packages/payload/packages/payments/src/Invoice/index.ts b/packages/payload/packages/payments/src/Invoice/index.ts new file mode 100644 index 000000000..2878663f2 --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/index.ts @@ -0,0 +1,2 @@ +export * from './getInvoiceForEscrow.ts' +export * from './Invoice.ts' diff --git a/packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts new file mode 100644 index 000000000..04d2059c4 --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts @@ -0,0 +1,69 @@ +import { assertEx } from '@xylabs/assert' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' + +import { NO_DISCOUNT, PaymentDiscountDiviner } from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' +import type { PaymentTotalDivinerConfig } from '../../Total/index.ts' +import { PaymentTotalDiviner } from '../../Total/index.ts' +import { getInvoiceForEscrow } from '../getInvoiceForEscrow.ts' + +describe('getInvoiceForEscrow', () => { + let node: MemoryNode + let paymentDiscountDiviner: PaymentDiscountDiviner + let paymentSubtotalDiviner: PaymentSubtotalDiviner + let paymentTotalDiviner: PaymentTotalDiviner + beforeAll(async () => { + node = await MemoryNode.create({ account: 'random' }) + paymentDiscountDiviner = await PaymentDiscountDiviner.create({ account: 'random' }) + paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) + const config: PaymentTotalDivinerConfig = { + paymentDiscountDiviner: paymentDiscountDiviner.address, + paymentSubtotalDiviner: paymentSubtotalDiviner.address, + schema: PaymentTotalDiviner.defaultConfigSchema, + } + paymentTotalDiviner = await PaymentTotalDiviner.create({ account: 'random', config }) + const modules = [paymentDiscountDiviner, paymentSubtotalDiviner, paymentTotalDiviner] + for (const module of modules) { + await node.register(module) + await node.attach(module.address, true) + } + }) + describe('with no discount', () => { + it('should return invoice values', async () => { + const nbf = Date.now() + const exp = nbf + 1000 * 60 * 10 + const appraisal: HashLeaseEstimate = { + price: 10, currency: 'USD', schema: HashLeaseEstimateSchema, exp, nbf, + } + const appraisalHash = await PayloadBuilder.dataHash(appraisal) + const terms: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [appraisalHash], exp, nbf, + } + const dataHashMap = await PayloadBuilder.toDataHashMap([appraisal]) + const result = await getInvoiceForEscrow(terms, dataHashMap, paymentTotalDiviner) + expect(result).toBeArray() + expect(result?.length).toBeGreaterThan(0) + const invoice = assertEx(result) + const [subtotal, total, payment, discount] = invoice + expect(subtotal).toBeDefined() + expect(subtotal.amount).toBeNumber() + expect(subtotal.amount).toBe(appraisal.price) + expect(subtotal.currency).toBe('USD') + expect(total).toBeDefined() + expect(total.amount).toBeNumber() + expect(total.amount).toBe(subtotal.amount) + expect(total.currency).toBe('USD') + expect(payment).toBeDefined() + expect(payment.amount).toBeNumber() + expect(payment.amount).toBe(total.amount) + expect(payment.currency).toBe('USD') + expect(discount).toBeDefined() + expect(discount).toMatchObject(NO_DISCOUNT) + }) + }) +}) diff --git a/packages/payload/packages/payments/src/Invoice/spec/tsconfig.json b/packages/payload/packages/payments/src/Invoice/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/Subtotal/Config.ts b/packages/payload/packages/payments/src/Subtotal/Config.ts new file mode 100644 index 000000000..8eb263689 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Config.ts @@ -0,0 +1,13 @@ +// import type { Hash } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' + +export const PaymentSubtotalDivinerConfigSchema = 'network.xyo.diviner.payments.subtotal.config' +export type PaymentSubtotalDivinerConfigSchema = typeof PaymentSubtotalDivinerConfigSchema + +/** + * The configuration for the Coupon Subtotal Diviner + */ +export type PaymentSubtotalDivinerConfig = DivinerConfig< + {}, + PaymentSubtotalDivinerConfigSchema +> diff --git a/packages/payload/packages/payments/src/Subtotal/Diviner.ts b/packages/payload/packages/payments/src/Subtotal/Diviner.ts new file mode 100644 index 000000000..8491c1d9e --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Diviner.ts @@ -0,0 +1,66 @@ +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import { Payload } from '@xyo-network/payload-model' +import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { PaymentSubtotalDivinerConfigSchema } from './Config.ts' +import { + appraisalValidators, termsValidators, ValidEscrowTerms, +} from './lib/index.ts' +import { PaymentSubtotalDivinerParams } from './Params.ts' +import { Subtotal, SubtotalSchema } from './Payload.ts' + +const currency = 'USD' + +/** + * Escrow terms that contain all the valid fields for calculating a subtotal + */ +export type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload + +@creatableModule() +export class PaymentSubtotalDiviner< + TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams, + TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType, + TOut extends Subtotal = Subtotal, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentSubtotalDivinerConfigSchema] + static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema + + protected async divineHandler(payloads: TIn[] = []): Promise { + // Find the escrow terms + const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined + if (!terms) return [] + + // Run all terms validations + if (!termsValidators.every(validator => validator(terms))) return [] + const validTerms = terms as ValidEscrowTerms + + // Retrieve all appraisals from terms + const hashMap = await PayloadBuilder.toAllHashMap(payloads) + const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] + + // Ensure all appraisals are present + if (appraisals.length !== validTerms.appraisals.length) return [] + + // Run all appraisal validations + if (!appraisalValidators.every(validator => validator(appraisals))) return [] + const amount = calculateSubtotal(appraisals) + const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals] + return [{ + amount, currency, schema: SubtotalSchema, sources, + }] as TOut[] + } +} + +// TODO: Add support for other currencies +const calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => { + return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0) +} diff --git a/packages/payload/packages/payments/src/Subtotal/Params.ts b/packages/payload/packages/payments/src/Subtotal/Params.ts new file mode 100644 index 000000000..7e05fbc3a --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Params.ts @@ -0,0 +1,8 @@ +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentSubtotalDivinerConfig } from './Config.ts' + +export type PaymentSubtotalDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payload/packages/payments/src/Subtotal/Payload.ts b/packages/payload/packages/payments/src/Subtotal/Payload.ts new file mode 100644 index 000000000..cce562286 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Payload.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../Amount/index.ts' + +export const SubtotalSchema = 'network.xyo.payments.subtotal' as const +export type SubtotalSchema = typeof SubtotalSchema + +export interface SubtotalFields extends AmountFields {} + +/** + * The result of a subtotal + */ +export type Subtotal = PayloadWithSources + +/** + * Identity function for determining if an object is an Subtotal + */ +export const isSubtotal = isPayloadOfSchemaType(SubtotalSchema) + +/** + * Identity function for determining if an object is an Subtotal with sources + */ +export const isSubtotalWithSources = isPayloadOfSchemaTypeWithSources(SubtotalSchema) + +/** + * Identity function for determining if an object is an Subtotal with meta + */ +export const isSubtotalWithMeta = isPayloadOfSchemaTypeWithMeta(SubtotalSchema) diff --git a/packages/payload/packages/payments/src/Subtotal/index.ts b/packages/payload/packages/payments/src/Subtotal/index.ts new file mode 100644 index 000000000..7a0ddb968 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/index.ts @@ -0,0 +1,4 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './Params.ts' +export * from './Payload.ts' diff --git a/packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts b/packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts new file mode 100644 index 000000000..94f009b80 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts @@ -0,0 +1,45 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' + +import { isIso4217CurrencyCode } from '../../Amount/index.ts' +import { validateDuration } from './durationValidators.ts' + +const validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => { + // Ensure all appraisals are numeric + if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false + // Ensure all appraisals are positive numbers + if (appraisals.some(appraisal => appraisal.price < 0)) return false + return true +} + +const validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => { + // NOTE: Only supporting USD for now, the remaining checks are for future-proofing. + if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false + + // Check every object in the array to ensure they all are in a supported currency. + if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false + + return true +} + +const validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => { + // Check if the array is empty or contains only one element, no need to compare. + if (appraisals.length <= 1) return true + + // Get the currency of the first element to compare with others. + const { currency } = appraisals[0] + if (!currency) return false + + // Check every object in the array to ensure they all have the same currency. + if (!appraisals.every(item => item.currency === currency)) return false + + return true +} + +const validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration) + +export const appraisalValidators = [ + validateAppraisalAmount, + validateAppraisalCurrency, + validateAppraisalConsistentCurrency, + validateAppraisalWindow, +] diff --git a/packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts b/packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts new file mode 100644 index 000000000..52831e574 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts @@ -0,0 +1,18 @@ +import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' + +const FIVE_MINUTES = 1000 * 60 * 5 + +/** + * Validates that the current time is within the duration window, within a configurable a buffer + * @param value The duration value + * @param windowMs The window in milliseconds to allow for a buffer + * @returns True if the duration is valid, false otherwise + */ +export const validateDuration = (value: Partial, windowMs = FIVE_MINUTES): boolean => { + const now = Date.now() + if (!value.nbf || value.nbf > now) return false + // If already expired (include for a 5 minute buffer to allow for a reasonable + // minimum amount of time for the transaction to be processed) + if (!value.exp || value.exp - now < windowMs) return false + return true +} diff --git a/packages/payload/packages/payments/src/Subtotal/lib/index.ts b/packages/payload/packages/payments/src/Subtotal/lib/index.ts new file mode 100644 index 000000000..c454d02eb --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './appraisalValidators.ts' +export * from './termsValidators.ts' diff --git a/packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts b/packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts new file mode 100644 index 000000000..c447a824b --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts @@ -0,0 +1,18 @@ +import type { Hash } from '@xylabs/hex' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { validateDuration } from './durationValidators.ts' + +export type ValidEscrowTerms = Required + +const validateTermsAppraisals = (terms: EscrowTerms): terms is Required => { + if (!terms.appraisals) return false + if (terms.appraisals.length === 0) return false + return true +} +const validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms) + +export const termsValidators = [ + validateTermsAppraisals, + validateTermsWindow, +] diff --git a/packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts b/packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts new file mode 100644 index 000000000..b31c90e33 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts @@ -0,0 +1,110 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeAll, beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentSubtotalDiviner } from '../Diviner.ts' +import { + isSubtotal, + SubtotalSchema, +} from '../Payload.ts' + +describe('PaymentSubtotalDiviner', () => { + let sut: PaymentSubtotalDiviner + const nbf = Date.now() + const exp = Number.MAX_SAFE_INTEGER + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ + [ + [ + { + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + ], 11], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, + }, + ], 30], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + ], 300], + ] + beforeEach(() => { + vi.clearAllMocks() + }) + beforeAll(async () => { + sut = await PaymentSubtotalDiviner.create({ account: 'random' }) + }) + describe('with escrow terms containing valid appraisals', () => { + it.each(cases)('calculates the subtotal of all the appraisals', async (appraisals, total) => { + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isSubtotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: SubtotalSchema }) + expect(result?.sources).toEqual([await PayloadBuilder.dataHash(terms), ...appraisalHashes]) + }) + }) + describe('with escrow terms containing invalid appraisals', () => { + describe('when containing negative values in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: -1, currency: 'USD', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + describe('when containing non-numeric values in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: 'three dollars', currency: 'USD', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + describe('when containing mixed currencies in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, { + schema: HashLeaseEstimateSchema, price: 1, currency: 'EUR', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + }) +}) diff --git a/packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json b/packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/Total/Config.ts b/packages/payload/packages/payments/src/Total/Config.ts new file mode 100644 index 000000000..143770b1a --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Config.ts @@ -0,0 +1,24 @@ +// import type { Hash } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' +import type { ModuleIdentifier } from '@xyo-network/module-model' + +export const PaymentTotalDivinerConfigSchema = 'network.xyo.diviner.payments.total.config' +export type PaymentTotalDivinerConfigSchema = typeof PaymentTotalDivinerConfigSchema + +/** + * The configuration for the Total Diviner + */ +export type PaymentTotalDivinerConfig = DivinerConfig< + { + /** + * The Diviner that will be used to determine the discount + */ + paymentDiscountDiviner?: ModuleIdentifier + + /** + * The Diviner that will be used to determine the subtotal + */ + paymentSubtotalDiviner?: ModuleIdentifier + }, + PaymentTotalDivinerConfigSchema +> diff --git a/packages/payload/packages/payments/src/Total/Diviner.ts b/packages/payload/packages/payments/src/Total/Diviner.ts new file mode 100644 index 000000000..9db4086c6 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Diviner.ts @@ -0,0 +1,70 @@ +import { assertEx } from '@xylabs/assert' +import { Hash } from '@xylabs/hex' +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { + asDivinerInstance, DivinerInstance, DivinerModuleEventData, +} from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' + +import { + Discount, + isDiscount, PaymentDiscountDiviner, + PaymentDiscountDivinerInputType, +} from '../Discount/index.ts' +import { + isSubtotal, PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType, Subtotal, +} from '../Subtotal/index.ts' +import { PaymentTotalDivinerConfigSchema } from './Config.ts' +import { PaymentTotalDivinerParams } from './Params.ts' +import { Total, TotalSchema } from './Payload.ts' + +type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType +type OutputType = Subtotal | Discount | Total + +@creatableModule() +export class PaymentTotalDiviner< + TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams, + TIn extends InputType = InputType, + TOut extends OutputType = OutputType, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentTotalDivinerConfigSchema] + static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema + + protected async divineHandler(payloads: TIn[] = []): Promise { + const subtotalDiviner = await this.getPaymentSubtotalDiviner() + const subtotalResult = await subtotalDiviner.divine(payloads) + const subtotal = subtotalResult.find(isSubtotal) + if (!subtotal) return [] + const discountDiviner = await this.getPaymentDiscountsDiviner() + const discountResult = await discountDiviner.divine(payloads) + const discount = discountResult.find(isDiscount) + if (!discount) return [] + const { currency: subtotalCurrency } = subtotal + const { currency: discountCurrency } = discount + assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`) + const amount = Math.max(0, subtotal.amount - discount.amount) + const currency = subtotalCurrency + const sources = [subtotal.$hash, discount.$hash] as Hash[] + const total: Total = { + amount, currency, sources, schema: TotalSchema, + } + return [subtotal, discount, total] as TOut[] + } + + protected async getPaymentDiscountsDiviner(): Promise { + const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner + } + + protected async getPaymentSubtotalDiviner(): Promise { + const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner + } +} diff --git a/packages/payload/packages/payments/src/Total/Params.ts b/packages/payload/packages/payments/src/Total/Params.ts new file mode 100644 index 000000000..07942f3ba --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Params.ts @@ -0,0 +1,8 @@ +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentTotalDivinerConfig } from './Config.ts' + +export type PaymentTotalDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payload/packages/payments/src/Total/Payload.ts b/packages/payload/packages/payments/src/Total/Payload.ts new file mode 100644 index 000000000..0e9e073f1 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Payload.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../Amount/index.ts' + +export const TotalSchema = 'network.xyo.payments.total' as const +export type TotalSchema = typeof TotalSchema + +export interface TotalFields extends AmountFields {} + +/** + * The result of a total + */ +export type Total = PayloadWithSources + +/** + * Identity function for determining if an object is an Total + */ +export const isTotal = isPayloadOfSchemaType(TotalSchema) + +/** + * Identity function for determining if an object is an Total with sources + */ +export const isTotalWithSources = isPayloadOfSchemaTypeWithSources(TotalSchema) + +/** + * Identity function for determining if an object is an Total with meta + */ +export const isTotalWithMeta = isPayloadOfSchemaTypeWithMeta(TotalSchema) diff --git a/packages/payload/packages/payments/src/Total/index.ts b/packages/payload/packages/payments/src/Total/index.ts new file mode 100644 index 000000000..7a0ddb968 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/index.ts @@ -0,0 +1,4 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './Params.ts' +export * from './Payload.ts' diff --git a/packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts b/packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts new file mode 100644 index 000000000..dbbce2ac6 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts @@ -0,0 +1,173 @@ +import { HDWallet } from '@xyo-network/account' +import { MemoryArchivist } from '@xyo-network/archivist-memory' +import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' +import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeAll, beforeEach, describe, it, vi, +} from 'vitest' + +import type { Coupon } from '../../Discount/index.ts' +import { + FixedAmountCouponSchema, FixedPercentageCouponSchema, PaymentDiscountDiviner, +} from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' +import { PaymentTotalDiviner } from '../Diviner.ts' +import { + isTotal, + TotalSchema, +} from '../Payload.ts' + +describe('PaymentTotalDiviner', () => { + let sut: PaymentTotalDiviner + const nbf = Date.now() + const exp = nbf + 1000 * 60 * 10 + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ + [ + [ + { + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + ], 11], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, + }, + ], 30], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + ], 300], + ] + const validCoupons: Coupon[] = [ + { + amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, + }, + ] + const unsignedCoupons: Coupon[] = [ + { + amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, + }, + ] + beforeEach(() => { + vi.clearAllMocks() + }) + beforeAll(async () => { + const node = await MemoryNode.create({ account: 'random' }) + expect(node).toBeDefined() + const paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) + const discountsArchivist = await MemoryArchivist.create({ account: 'random' }) + const signer = await HDWallet.random() + // Sign the valid coupons and insert them into the archivist + for (const coupon of validCoupons) { + const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() + await discountsArchivist.insert([bw, ...payloads]) + } + // Insert (but do not sign) the unsigned coupons into the archivist + await discountsArchivist.insert(unsignedCoupons) + const discountsBoundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ + account: 'random', + config: { + archivist: discountsArchivist.address, + schema: MemoryBoundWitnessDiviner.defaultConfigSchema, + }, + }) + const discountDiviner = await PaymentDiscountDiviner.create({ + account: 'random', + config: { + archivist: discountsArchivist.address, + boundWitnessDiviner: discountsBoundWitnessDiviner.address, + couponAuthorities: [signer.address], + schema: PaymentDiscountDiviner.defaultConfigSchema, + }, + }) + sut = await PaymentTotalDiviner.create({ + account: 'random', + config: { + paymentDiscountDiviner: discountDiviner.address, + paymentSubtotalDiviner: paymentSubtotalDiviner.address, + schema: PaymentTotalDiviner.defaultConfigSchema, + }, + }) + + const modules = [paymentSubtotalDiviner, discountsArchivist, discountsBoundWitnessDiviner, discountDiviner, sut] + for (const mod of modules) { + await node.register(mod) + await node.attach(mod.address, true) + } + }) + describe('with valid escrow', () => { + describe('with no discounts', () => { + it.each(cases)('calculates total', async (appraisals, total) => { + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: TotalSchema }) + }) + }) + describe('with valid discounts', () => { + describe.each(cases)('calculates total', (appraisals, total) => { + it.each(validCoupons)('applying coupon discount to total', async (coupon) => { + const discounts = await PayloadBuilder.dataHashes([coupon]) + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { + ...termsBase, appraisals: appraisalHashes, discounts, + } + const results = await sut.divine([terms, ...appraisals, coupon]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result?.amount).toBeLessThan(total) + }) + }) + }) + describe('with invalid discounts', () => { + describe.each(cases)('calculates total', (appraisals, total) => { + it.each(unsignedCoupons)('without applying coupon discount to total', async (coupon) => { + const discounts = await PayloadBuilder.dataHashes([coupon]) + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { + ...termsBase, appraisals: appraisalHashes, discounts, + } + const results = await sut.divine([terms, ...appraisals, coupon]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: TotalSchema }) + }) + }) + }) + }) +}) diff --git a/packages/payload/packages/payments/src/Total/spec/tsconfig.json b/packages/payload/packages/payments/src/Total/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payload/packages/payments/src/Total/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/index.ts b/packages/payload/packages/payments/src/index.ts index 2825709d7..da64de4cb 100644 --- a/packages/payload/packages/payments/src/index.ts +++ b/packages/payload/packages/payments/src/index.ts @@ -1,6 +1,11 @@ +export * from './Amount/index.ts' export * from './Billing/index.ts' export * from './Currency.ts' +export * from './Discount/index.ts' export * from './Escrow/index.ts' +export * from './Invoice/index.ts' export * from './Payment/index.ts' export * from './Purchase/index.ts' export * from './Receipt/index.ts' +export * from './Subtotal/index.ts' +export * from './Total/index.ts' diff --git a/packages/payloadset/packages/payments/.depcheckrc b/packages/payloadset/packages/payments/.depcheckrc new file mode 100644 index 000000000..e65712127 --- /dev/null +++ b/packages/payloadset/packages/payments/.depcheckrc @@ -0,0 +1,3 @@ +ignores: [ + "jest" +] \ No newline at end of file diff --git a/packages/payloadset/packages/payments/.npmignore b/packages/payloadset/packages/payments/.npmignore new file mode 100644 index 000000000..0430e8967 --- /dev/null +++ b/packages/payloadset/packages/payments/.npmignore @@ -0,0 +1,21 @@ +.* +.env +.eslintcache +.example.env +tsconfig* +jest.config.js +rollup.config.ts +yarn.lock +**/*.spec.ts +**/*.snap + +.github +docs +.pnp.* +.vscode +.yarn/* +coverage +cspell.json +node_modules +swagger.json +packages \ No newline at end of file diff --git a/packages/payloadset/packages/payments/LICENSE b/packages/payloadset/packages/payments/LICENSE new file mode 100644 index 000000000..0a041280b --- /dev/null +++ b/packages/payloadset/packages/payments/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/packages/payloadset/packages/payments/README.md b/packages/payloadset/packages/payments/README.md new file mode 100644 index 000000000..04d7c668c --- /dev/null +++ b/packages/payloadset/packages/payments/README.md @@ -0,0 +1,13 @@ +[![logo][]](https://xyo.network) + +Part of [sdk-xyo-client-js](https://www.npmjs.com/package/@xyo-network/sdk-xyo-client-js) + +## License + +> See the [LICENSE](LICENSE) file for license details + +## Credits + +[Made with 🔥 and ❄️ by XYO](https://xyo.network) + +[logo]: https://cdn.xy.company/img/brand/XYO_full_colored.png \ No newline at end of file diff --git a/packages/payloadset/packages/payments/package.json b/packages/payloadset/packages/payments/package.json new file mode 100644 index 000000000..65655c445 --- /dev/null +++ b/packages/payloadset/packages/payments/package.json @@ -0,0 +1,54 @@ +{ + "name": "@xyo-network/payment-payloadset-plugins", + "version": "3.0.17", + "description": "Typescript/Javascript Plugins for XYO Platform", + "homepage": "https://xyo.network", + "bugs": { + "url": "git+https://github.com/XYOracleNetwork/plugins/issues", + "email": "support@xyo.network" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/XYOracleNetwork/plugins.git" + }, + "license": "LGPL-3.0-only", + "author": { + "name": "XYO Development Team", + "email": "support@xyo.network", + "url": "https://xyo.network" + }, + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/neutral/index.d.ts", + "default": "./dist/neutral/index.mjs" + }, + "./package.json": "./package.json" + }, + "module": "dist/neutral/index.mjs", + "types": "dist/neutral/index.d.ts", + "dependencies": { + "@xylabs/assert": "^4.0.9", + "@xylabs/axios": "^4.0.9", + "@xyo-network/module-model": "^3.1.9", + "@xyo-network/payload-builder": "^3.1.9", + "@xyo-network/payload-model": "^3.1.9", + "@xyo-network/payment-payload-plugins": "workspace:^", + "@xyo-network/rebilly-payment-payload-plugin": "workspace:^", + "@xyo-network/sentinel-abstract": "^3.1.9", + "@xyo-network/sentinel-model": "^3.1.9", + "axios": "^1.7.7" + }, + "devDependencies": { + "@xylabs/jest-helpers": "^4.0.9", + "@xylabs/ts-scripts-yarn3": "^4.0.7", + "@xylabs/tsconfig": "^4.0.7", + "@xyo-network/boundwitness-model": "^3.1.9", + "jest": "^29.7.0", + "typescript": "^5.5.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts new file mode 100644 index 000000000..2344e0398 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts @@ -0,0 +1,557 @@ +/* eslint-disable max-lines */ +/** + * ISO 4217 currency codes + */ +export type Iso4217AlphabeticalCode = + 'AED' | + 'AFN' | + 'ALL' | + 'AMD' | + 'ANG' | + 'AOA' | + 'ARS' | + 'AUD' | + 'AWG' | + 'AZN' | + 'BAM' | + 'BBD' | + 'BDT' | + 'BGN' | + 'BHD' | + 'BIF' | + 'BMD' | + 'BND' | + 'BOB' | + 'BOV' | + 'BRL' | + 'BSD' | + 'BTN' | + 'BWP' | + 'BYN' | + 'BZD' | + 'CAD' | + 'CDF' | + 'CHE' | + 'CHF' | + 'CHW' | + 'CLF' | + 'CLP' | + 'CNY' | + 'COP' | + 'COU' | + 'CRC' | + 'CUP' | + 'CVE' | + 'CZK' | + 'DJF' | + 'DKK' | + 'DOP' | + 'DZD' | + 'EGP' | + 'ERN' | + 'ETB' | + 'EUR' | + 'FJD' | + 'FKP' | + 'GBP' | + 'GEL' | + 'GHS' | + 'GIP' | + 'GMD' | + 'GNF' | + 'GTQ' | + 'GYD' | + 'HKD' | + 'HNL' | + 'HTG' | + 'HUF' | + 'IDR' | + 'ILS' | + 'INR' | + 'IQD' | + 'IRR' | + 'ISK' | + 'JMD' | + 'JOD' | + 'JPY' | + 'KES' | + 'KGS' | + 'KHR' | + 'KMF' | + 'KPW' | + 'KRW' | + 'KWD' | + 'KYD' | + 'KZT' | + 'LAK' | + 'LBP' | + 'LKR' | + 'LRD' | + 'LSL' | + 'LYD' | + 'MAD' | + 'MDL' | + 'MGA' | + 'MKD' | + 'MMK' | + 'MNT' | + 'MOP' | + 'MRU' | + 'MUR' | + 'MVR' | + 'MWK' | + 'MXN' | + 'MXV' | + 'MYR' | + 'MZN' | + 'NAD' | + 'NGN' | + 'NIO' | + 'NOK' | + 'NPR' | + 'NZD' | + 'OMR' | + 'PAB' | + 'PEN' | + 'PGK' | + 'PHP' | + 'PKR' | + 'PLN' | + 'PYG' | + 'QAR' | + 'RON' | + 'RSD' | + 'RUB' | + 'RWF' | + 'SAR' | + 'SBD' | + 'SCR' | + 'SDG' | + 'SEK' | + 'SGD' | + 'SHP' | + 'SLE' | + 'SOS' | + 'SRD' | + 'SSP' | + 'STN' | + 'SVC' | + 'SYP' | + 'SZL' | + 'THB' | + 'TJS' | + 'TMT' | + 'TND' | + 'TOP' | + 'TRY' | + 'TTD' | + 'TWD' | + 'TZS' | + 'UAH' | + 'UGX' | + 'USD' | + 'USN' | + 'UYI' | + 'UYU' | + 'UYW' | + 'UZS' | + 'VED' | + 'VES' | + 'VND' | + 'VUV' | + 'WST' | + 'XAF' | + 'XAG' | + 'XAU' | + 'XBA' | + 'XBB' | + 'XBC' | + 'XBD' | + 'XCD' | + 'XDR' | + 'XOF' | + 'XPD' | + 'XPF' | + 'XPT' | + 'XSU' | + 'XTS' | + 'XUA' | + 'XXX' | + 'YER' | + 'ZAR' | + 'ZMW' | + 'ZWG' | + 'ZWL' + +// TODO: Technically, the values should be 3 digit numbers with leading +// zeros, so we can padStart if we need to be strict +/** + * ISO 4217 numeric currency number codes + */ +export type Iso4217NumericCode = + 784 | + 971 | + 8 | + 51 | + 532 | + 973 | + 32 | + 36 | + 533 | + 944 | + 977 | + 52 | + 50 | + 975 | + 48 | + 108 | + 60 | + 96 | + 68 | + 984 | + 986 | + 44 | + 64 | + 72 | + 933 | + 84 | + 124 | + 976 | + 947 | + 756 | + 948 | + 990 | + 152 | + 156 | + 170 | + 970 | + 188 | + 192 | + 132 | + 203 | + 262 | + 208 | + 214 | + 12 | + 818 | + 232 | + 230 | + 978 | + 242 | + 238 | + 826 | + 981 | + 936 | + 292 | + 270 | + 324 | + 320 | + 328 | + 344 | + 340 | + 332 | + 348 | + 360 | + 376 | + 356 | + 368 | + 364 | + 352 | + 388 | + 400 | + 392 | + 404 | + 417 | + 116 | + 174 | + 408 | + 410 | + 414 | + 136 | + 398 | + 418 | + 422 | + 144 | + 430 | + 426 | + 434 | + 504 | + 498 | + 969 | + 807 | + 104 | + 496 | + 446 | + 929 | + 480 | + 462 | + 454 | + 484 | + 979 | + 458 | + 943 | + 516 | + 566 | + 558 | + 578 | + 524 | + 554 | + 512 | + 590 | + 604 | + 598 | + 608 | + 586 | + 985 | + 600 | + 634 | + 946 | + 941 | + 643 | + 646 | + 682 | + 90 | + 690 | + 938 | + 752 | + 702 | + 654 | + 925 | + 706 | + 968 | + 728 | + 930 | + 222 | + 760 | + 748 | + 764 | + 972 | + 934 | + 788 | + 776 | + 949 | + 780 | + 901 | + 834 | + 980 | + 800 | + 840 | + 997 | + 940 | + 858 | + 927 | + 860 | + 926 | + 928 | + 704 | + 548 | + 882 | + 950 | + 961 | + 959 | + 955 | + 956 | + 957 | + 958 | + 951 | + 960 | + 952 | + 964 | + 953 | + 962 | + 994 | + 963 | + 965 | + 999 | + 886 | + 710 | + 967 | + 924 | + 932 + +/** + * Dictionary of ISO 4217 alphabetical currency codes to numeric currency number codes + */ +export const Iso4217CurrencyCodes: Record = { + AED: 784, + AFN: 971, + ALL: 8, + AMD: 51, + ANG: 532, + AOA: 973, + ARS: 32, + AUD: 36, + AWG: 533, + AZN: 944, + BAM: 977, + BBD: 52, + BDT: 50, + BGN: 975, + BHD: 48, + BIF: 108, + BMD: 60, + BND: 96, + BOB: 68, + BOV: 984, + BRL: 986, + BSD: 44, + BTN: 64, + BWP: 72, + BYN: 933, + BZD: 84, + CAD: 124, + CDF: 976, + CHE: 947, + CHF: 756, + CHW: 948, + CLF: 990, + CLP: 152, + CNY: 156, + COP: 170, + COU: 970, + CRC: 188, + CUP: 192, + CVE: 132, + CZK: 203, + DJF: 262, + DKK: 208, + DOP: 214, + DZD: 12, + EGP: 818, + ERN: 232, + ETB: 230, + EUR: 978, + FJD: 242, + FKP: 238, + GBP: 826, + GEL: 981, + GHS: 936, + GIP: 292, + GMD: 270, + GNF: 324, + GTQ: 320, + GYD: 328, + HKD: 344, + HNL: 340, + HTG: 332, + HUF: 348, + IDR: 360, + ILS: 376, + INR: 356, + IQD: 368, + IRR: 364, + ISK: 352, + JMD: 388, + JOD: 400, + JPY: 392, + KES: 404, + KGS: 417, + KHR: 116, + KMF: 174, + KPW: 408, + KRW: 410, + KWD: 414, + KYD: 136, + KZT: 398, + LAK: 418, + LBP: 422, + LKR: 144, + LRD: 430, + LSL: 426, + LYD: 434, + MAD: 504, + MDL: 498, + MGA: 969, + MKD: 807, + MMK: 104, + MNT: 496, + MOP: 446, + MRU: 929, + MUR: 480, + MVR: 462, + MWK: 454, + MXN: 484, + MXV: 979, + MYR: 458, + MZN: 943, + NAD: 516, + NGN: 566, + NIO: 558, + NOK: 578, + NPR: 524, + NZD: 554, + OMR: 512, + PAB: 590, + PEN: 604, + PGK: 598, + PHP: 608, + PKR: 586, + PLN: 985, + PYG: 600, + QAR: 634, + RON: 946, + RSD: 941, + RUB: 643, + RWF: 646, + SAR: 682, + SBD: 90, + SCR: 690, + SDG: 938, + SEK: 752, + SGD: 702, + SHP: 654, + SLE: 925, + SOS: 706, + SRD: 968, + SSP: 728, + STN: 930, + SVC: 222, + SYP: 760, + SZL: 748, + THB: 764, + TJS: 972, + TMT: 934, + TND: 788, + TOP: 776, + TRY: 949, + TTD: 780, + TWD: 901, + TZS: 834, + UAH: 980, + UGX: 800, + USD: 840, + USN: 997, + UYI: 940, + UYU: 858, + UYW: 927, + UZS: 860, + VED: 926, + VES: 928, + VND: 704, + VUV: 548, + WST: 882, + XAF: 950, + XAG: 961, + XAU: 959, + XBA: 955, + XBB: 956, + XBC: 957, + XBD: 958, + XCD: 951, + XDR: 960, + XOF: 952, + XPD: 964, + XPF: 953, + XPT: 962, + XSU: 994, + XTS: 963, + XUA: 965, + XXX: 999, + YER: 886, + ZAR: 710, + ZMW: 967, + ZWG: 924, + ZWL: 932, +} + +export const isIso4217CurrencyCode = (code: string): code is Iso4217AlphabeticalCode => Iso4217CurrencyCodes[code as Iso4217AlphabeticalCode] ? true : false diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts new file mode 100644 index 000000000..bfd293c60 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts @@ -0,0 +1,36 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { Iso4217AlphabeticalCode } from './Iso4217Currency.ts' + +export const AmountSchema = 'network.xyo.payments.amount' as const +export type AmountSchema = typeof AmountSchema + +export interface AmountFields { + amount: number + currency: Iso4217AlphabeticalCode +} + +/** + * The result of a amount + */ +export type Amount = PayloadWithSources + +/** + * Identity function for determining if an object is an Amount + */ +export const isAmount = isPayloadOfSchemaType(AmountSchema) + +/** + * Identity function for determining if an object is an Amount with sources + */ +export const isAmountWithSources = isPayloadOfSchemaTypeWithSources(AmountSchema) + +/** + * Identity function for determining if an object is an Amount with meta + */ +export const isAmountWithMeta = isPayloadOfSchemaTypeWithMeta(AmountSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts new file mode 100644 index 000000000..155f9afe3 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts @@ -0,0 +1,2 @@ +export * from './Iso4217Currency.ts' +export * from './Payload.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts new file mode 100644 index 000000000..842b2c64c --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts @@ -0,0 +1,34 @@ +// import type { Hash } from '@xylabs/hex' +import type { Address } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' +import type { ModuleIdentifier } from '@xyo-network/module-model' + +export const PaymentDiscountDivinerConfigSchema = 'network.xyo.diviner.payments.discount.config' +export type PaymentDiscountDivinerConfigSchema = typeof PaymentDiscountDivinerConfigSchema + +/** + * The configuration for the Payment Discount Diviner + */ +export type PaymentDiscountDivinerConfig = DivinerConfig< + { + /** + * The boundwitness diviner used to query for payloads + */ + boundWitnessDiviner?: ModuleIdentifier + /** + * The list of coupon authorities that can be used to get a discount + */ + couponAuthorities?: Address[] + + // /** + // * The list of coupons that are supported by this diviner + // */ + // supportedCoupons?: Hash[] + + /** + * The Diviner that can be used to determine the subtotal to apply discounts to + */ + paymentSubtotalDiviner?: ModuleIdentifier + }, + PaymentDiscountDivinerConfigSchema +> diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts new file mode 100644 index 000000000..aeaedc751 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts @@ -0,0 +1,158 @@ +import { assertEx } from '@xylabs/assert' +import { exists } from '@xylabs/exists' +import { Address, Hash } from '@xylabs/hex' +import { ArchivistInstance, asArchivistInstance } from '@xyo-network/archivist-model' +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model' +import { + HashLeaseEstimate, + isHashLeaseEstimate, +} from '@xyo-network/diviner-hash-lease' +import { + asDivinerInstance, DivinerInstance, DivinerModuleEventData, +} from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import { Payload } from '@xyo-network/payload-model' +import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { PaymentDiscountDivinerConfigSchema } from './Config.ts' +import { applyCoupons } from './lib/index.ts' +import { PaymentDiscountDivinerParams } from './Params.ts' +import { + Coupon, + Discount, + isCoupon, + isCouponWithMeta, + NO_DISCOUNT, +} from './Payload/index.ts' + +const DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly = { + limit: 1, + order: 'desc', + schema: BoundWitnessDivinerQuerySchema, +} + +export type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload + +@creatableModule() +export class PaymentDiscountDiviner< + TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams, + TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType, + TOut extends Discount = Discount, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentDiscountDivinerConfigSchema] + static override defaultConfigSchema = PaymentDiscountDivinerConfigSchema + + protected get couponAuthorities(): Address[] { + return [...(this.config.couponAuthorities ?? []), ...(this.params.couponAuthorities ?? [])] + } + + protected async divineHandler(payloads: TIn[] = []): Promise { + const sources: Hash[] = [] + + // Parse terms + const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined + if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[] + sources.push(await PayloadBuilder.hash(terms)) + + // Parse discounts + const discountHashes = terms.discounts ?? [] + if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + // TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic + // Parse appraisals + const termsAppraisals = terms?.appraisals + if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + const hashMap = await PayloadBuilder.toAllHashMap(payloads) + const foundAppraisals = termsAppraisals.filter(hash => hashMap[hash]) + // Add the appraisals that were found to the sources + sources.push(...foundAppraisals) + // If not all appraisals are found, return no discount + if (foundAppraisals.length !== termsAppraisals.length) { + return [{ ...NO_DISCOUNT, sources }] as TOut[] + } + // TODO: Cast should not be required + const appraisals = foundAppraisals.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] + + // Use the supplied payloads to find the discounts + const discounts = discountHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) as Coupon[] + // Find any remaining coupons from the archivist + if (discounts.length !== discountHashes.length) { + // Find remaining from discounts archivist + const discountsArchivist = await this.getDiscountsArchivist() + const foundDiscounts = await discountsArchivist.get(discountHashes) + discounts.push(...foundDiscounts.filter(isCouponWithMeta)) + } + const discountsMap = await PayloadBuilder.toAllHashMap(discounts) + if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + // Add the found discounts to the sources + const foundDiscountsHashes = Object.keys(discountsMap) as Hash[] + sources.push(...foundDiscountsHashes) + + // Log individual discounts that were not found + for (const hash of discountHashes) { + if (!foundDiscountsHashes.includes(hash)) { + console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`) + } + } + + // Parse coupons + const coupons = Object.values(discountsMap) + const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent)) + if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] + + const discount = applyCoupons(appraisals, validCoupons) + return [{ ...discount, sources }] as TOut[] + } + + /** + * Filters the supplied list of coupons to only those that are signed by + * addresses specified in the couponAuthorities + * @param coupons The list of coupons to filter + * @returns The filtered list of coupons that are signed by the couponAuthorities + */ + protected async filterToSigned(coupons: Coupon[]): Promise { + const signed: Coupon[] = [] + const dataHashMap = await PayloadBuilder.toDataHashMap(coupons) + const boundWitnessDiviner = await this.getDiscountsBoundWitnessDiviner() + const hashes = Object.keys(dataHashMap) + const addresses = this.couponAuthorities + // TODO: Keep an in memory cache of the hashes queried and their results + // to avoid querying the same hash multiple times + await Promise.all(hashes.map((h) => { + const hash = h as Hash + return Promise.all(addresses.map(async (address) => { + const query: BoundWitnessDivinerQueryPayload = { + ...DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS, addresses: [address], payload_hashes: [hash], + } + const result = await boundWitnessDiviner.divine([query]) + if (result.length > 0) signed.push(dataHashMap[hash]) + })) + })) + return signed + } + + protected async getDiscountsArchivist(): Promise { + const name = assertEx(this.config.archivist, () => 'Missing archivist in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving archivist: ${name}`) + return assertEx(asArchivistInstance(mod), () => `Resolved module ${mod.address} not a valid Archivist`) + } + + protected async getDiscountsBoundWitnessDiviner(): Promise { + const name = assertEx(this.config.boundWitnessDiviner, () => 'Missing boundWitnessDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) + } + + protected isCouponCurrent(coupon: Coupon): boolean { + const now = Date.now() + return coupon.exp > now && coupon.nbf < now + } +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts new file mode 100644 index 000000000..8ffb0774e --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts @@ -0,0 +1,14 @@ +import type { Address } from '@xylabs/hex' +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentDiscountDivinerConfig } from './Config.ts' + +export type PaymentDiscountDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts new file mode 100644 index 000000000..e8ea3e731 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts @@ -0,0 +1,37 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../../../../Amount/index.ts' +import { CouponSchema } from '../Schema.ts' +import type { CouponFields } from '../types/index.ts' + +export const FixedAmountCouponSchema = `${CouponSchema}.fixed.amount` as const +export type FixedAmountCouponSchema = typeof FixedAmountCouponSchema + +export interface FixedAmountCouponFields extends CouponFields, AmountFields { + +} + +/** + * A coupon that provides a fixed discount amount + */ +export type FixedAmountCoupon = PayloadWithSources + +/** + * Identity function for determining if an object is an FixedAmountCoupon + */ +export const isFixedAmountCoupon = isPayloadOfSchemaType(FixedAmountCouponSchema) + +/** + * Identity function for determining if an object is an FixedAmountCoupon with sources + */ +export const isFixedAmountCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedAmountCouponSchema) + +/** + * Identity function for determining if an object is an FixedAmountCoupon with meta + */ +export const isFixedAmountCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedAmountCouponSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts new file mode 100644 index 000000000..1d6942e69 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts @@ -0,0 +1,37 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import { CouponSchema } from '../Schema.ts' +import type { CouponFields } from '../types/index.ts' + +export const FixedPercentageCouponSchema = `${CouponSchema}.fixed.percentage` as const +export type FixedPercentageCouponSchema = typeof FixedPercentageCouponSchema + +export interface FixedPercentageCouponFields extends CouponFields { + percentage: number + +} + +/** + * A coupon that provides a fixed discount amount + */ +export type FixedPercentageCoupon = PayloadWithSources + +/** + * Identity function for determining if an object is an FixedPercentageCoupon + */ +export const isFixedPercentageCoupon = isPayloadOfSchemaType(FixedPercentageCouponSchema) + +/** + * Identity function for determining if an object is an FixedPercentageCoupon with sources + */ +export const isFixedPercentageCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedPercentageCouponSchema) + +/** + * Identity function for determining if an object is an FixedPercentageCoupon with meta + */ +export const isFixedPercentageCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedPercentageCouponSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts new file mode 100644 index 000000000..ff31c45af --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts @@ -0,0 +1,2 @@ +export * from './FixedAmount.ts' +export * from './FixedPercentage.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts new file mode 100644 index 000000000..40bae5a2a --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts @@ -0,0 +1,25 @@ +import { + type FixedAmountCoupon, type FixedPercentageCoupon, isFixedAmountCoupon, isFixedAmountCouponWithMeta, isFixedAmountCouponWithSources, isFixedPercentageCoupon, + isFixedPercentageCouponWithMeta, + isFixedPercentageCouponWithSources, +} from './Coupons/index.ts' + +/** + * The result of a discount + */ +export type Coupon = FixedAmountCoupon | FixedPercentageCoupon + +/** + * Identity function for determining if an object is an Coupon + */ +export const isCoupon = (x?: unknown | null) => isFixedAmountCoupon(x) || isFixedPercentageCoupon(x) + +/** + * Identity function for determining if an object is an Coupon with sources + */ +export const isCouponWithSources = (x?: unknown | null) => isFixedAmountCouponWithSources(x) || isFixedPercentageCouponWithSources(x) + +/** + * Identity function for determining if an object is an Coupon with meta + */ +export const isCouponWithMeta = (x?: unknown | null) => isFixedAmountCouponWithMeta(x) || isFixedPercentageCouponWithMeta(x) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts new file mode 100644 index 000000000..4cf0542af --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts @@ -0,0 +1,2 @@ +export const CouponSchema = 'network.xyo.payments.coupon' as const +export type CouponSchema = typeof CouponSchema diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts new file mode 100644 index 000000000..02c47de6f --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts @@ -0,0 +1,4 @@ +export * from './Coupons/index.ts' +export * from './Payload.ts' +export * from './Schema.ts' +export * from './types/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts new file mode 100644 index 000000000..18e472798 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts @@ -0,0 +1,11 @@ +import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' + +/** + * The fields that are common across all coupons + */ +export interface CouponFields extends DurationFields { + /** + * Whether or not this discount can be stacked with other discounts + */ + stackable?: boolean +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts new file mode 100644 index 000000000..ee5290191 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts @@ -0,0 +1,2 @@ +export * from './CouponFields.ts' +export * from './isStackable.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts new file mode 100644 index 000000000..a0fb58e29 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts @@ -0,0 +1,6 @@ +import type { CouponFields } from './CouponFields.ts' + +/** + * Identity function for determining if coupon is stackable + */ +export const isStackable = (x?: unknown | null) => (x as CouponFields ?? { stackable: false })?.stackable diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts new file mode 100644 index 000000000..f5fab74f8 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../../Amount/index.ts' + +export const DiscountSchema = 'network.xyo.payments.discount' as const +export type DiscountSchema = typeof DiscountSchema + +export interface DiscountFields extends AmountFields { } + +/** + * The result of a discount + */ +export type Discount = PayloadWithSources + +/** + * Identity function for determining if an object is an Discount + */ +export const isDiscount = isPayloadOfSchemaType(DiscountSchema) + +/** + * Identity function for determining if an object is an Discount with sources + */ +export const isDiscountWithSources = isPayloadOfSchemaTypeWithSources(DiscountSchema) + +/** + * Identity function for determining if an object is an Discount with meta + */ +export const isDiscountWithMeta = isPayloadOfSchemaTypeWithMeta(DiscountSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts new file mode 100644 index 000000000..8075f78a7 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts @@ -0,0 +1,8 @@ +import type { Discount } from './Discount.ts' +import { DiscountSchema } from './Discount.ts' + +export const NO_DISCOUNT: Discount = { + schema: DiscountSchema, + amount: 0, + currency: 'USD', +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts new file mode 100644 index 000000000..a39b81c5c --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts @@ -0,0 +1,3 @@ +export * from './Coupon/index.ts' +export * from './Discount.ts' +export * from './NoDiscount.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts new file mode 100644 index 000000000..6ee3fc3b0 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts @@ -0,0 +1,5 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './lib/index.ts' +export * from './Params.ts' +export * from './Payload/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts new file mode 100644 index 000000000..a079c7ea5 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts @@ -0,0 +1,59 @@ +import { assertEx } from '@xylabs/assert' +import { exists } from '@xylabs/exists' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' + +import type { AmountFields } from '../../Amount/index.ts' +import type { + Coupon, Discount, FixedAmountCoupon, + FixedPercentageCoupon, +} from '../Payload/index.ts' +import { + DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon, + isStackable, +} from '../Payload/index.ts' + +export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => { + // Ensure all appraisals and coupons are in USD + const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD') + assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD') + const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial)?.currency).filter(exists).every(currency => currency === 'USD') + assertEx(allCouponsAreUSD, 'All coupons must be in USD') + const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0) + + // Calculated non-stackable discount coupons + const singularFixedDiscount = Math.max(...coupons + .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon)) + .map(coupon => (coupon as FixedAmountCoupon).amount), 0) + const singularPercentageDiscount = (Math.max(...coupons + .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon)) + .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total + + // Calculate stackable discount coupons + // First calculate the total discount from fixed amount coupons + const stackedFixedDiscount = coupons + .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon)) + .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0) + // Then calculate the total discount from percentage coupons and apply + // the percentage discount to the remaining total after fixed discounts + const stackedPercentageDiscount = coupons + .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon)) + .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount) + // Sum all stackable discounts + const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount + + // Find the best coupon(s) to apply + const maxDiscount = Math.max( + singularFixedDiscount, + singularPercentageDiscount, + stackedDiscount, + 0, + ) + + // Ensure discount is not more than the total + const amount = Math.min(maxDiscount, total) + + // Return single discount payload + return { + amount, schema: DiscountSchema, currency: 'USD', + } +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts new file mode 100644 index 000000000..89c610e91 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts @@ -0,0 +1 @@ +export * from './applyCoupons.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts new file mode 100644 index 000000000..5a40c6a14 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts @@ -0,0 +1,86 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { + beforeEach, describe, it, vi, +} from 'vitest' + +import type { Coupon } from '../../Payload/index.ts' +import { + DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, +} from '../../Payload/index.ts' +import { applyCoupons } from '../applyCoupons.ts' + +describe('applyCoupons', () => { + const nbf = Date.now() + const exp = Date.now() + 10_000_000 + // Coupons + const TEN_DOLLAR_OFF_COUPON: Coupon = { + amount: 10, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', + } + const TEN_PERCENT_OFF_COUPON: Coupon = { + percentage: 0.1, exp, nbf, schema: FixedPercentageCouponSchema, + } + // Appraisals + const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const SEVENTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 70, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const THIRTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 30, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + describe('when coupon is less than total', () => { + const validCoupons: [HashLeaseEstimate[], Coupon[]][] = [ + [[HUNDRED_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], + [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], + [[HUNDRED_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], + [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], + ] + it.each(validCoupons)('Applies coupon discount', (estimates, coupons) => { + const results = applyCoupons(estimates, coupons) + expect(results).toEqual({ + amount: 10, schema: DiscountSchema, currency: 'USD', + }) + }) + }) + describe('when discount exceeds total', () => { + const discountExceedsTotal: [HashLeaseEstimate[], Coupon[]][] = [ + [ + [HUNDRED_DOLLAR_ESTIMATE], + [{ + amount: 101, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', + }]], + [ + [HUNDRED_DOLLAR_ESTIMATE], + [{ + percentage: 1.1, exp, nbf, schema: FixedPercentageCouponSchema, + }]], + ] + it.each(discountExceedsTotal)('Discounts only to total', (estimates, coupons) => { + const results = applyCoupons(estimates, coupons) + const amount = estimates.reduce((acc, a) => acc + a.price, 0) + expect(results).toEqual({ + amount, schema: DiscountSchema, currency: 'USD', + }) + }) + }) + describe('with stackable discounts', () => { + const STACKABLE_TEN_DOLLAR_OFF_COUPON: Coupon = { ...TEN_DOLLAR_OFF_COUPON, stackable: true } + const STACKABLE_TEN_PERCENT_OFF_COUPON: Coupon = { ...TEN_PERCENT_OFF_COUPON, stackable: true } + const validCoupons: [HashLeaseEstimate[], Coupon[], number][] = [ + [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_DOLLAR_OFF_COUPON, STACKABLE_TEN_PERCENT_OFF_COUPON], 19], + [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_PERCENT_OFF_COUPON, STACKABLE_TEN_DOLLAR_OFF_COUPON], 19], + ] + it.each(validCoupons)('Applies both discounts', (estimates, coupons, amount) => { + const results = applyCoupons(estimates, coupons) + expect(results).toEqual({ + amount, schema: DiscountSchema, currency: 'USD', + }) + }) + }) +}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts new file mode 100644 index 000000000..c7e32b7d3 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts @@ -0,0 +1,127 @@ +import { HDWallet } from '@xyo-network/account' +import { MemoryArchivist } from '@xyo-network/archivist-memory' +import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' +import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentDiscountDivinerConfigSchema } from '../Config.ts' +import { PaymentDiscountDiviner } from '../Diviner.ts' +import type { Coupon } from '../Payload/index.ts' +import { + DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, + isDiscount, +} from '../Payload/index.ts' + +describe('PaymentDiscountDiviner', () => { + let sut: PaymentDiscountDiviner + const nbf = Date.now() + const exp = Number.MAX_SAFE_INTEGER + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { + price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, + } + const validCoupons: Coupon[] = [ + { + amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, + }, + ] + const unsignedCoupons: Coupon[] = [ + { + amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, + }, + ] + beforeAll(async () => { + termsBase.appraisals?.push(await PayloadBuilder.hash(HUNDRED_DOLLAR_ESTIMATE)) + const node = await MemoryNode.create({ account: 'random' }) + const archivist = await MemoryArchivist.create({ account: 'random' }) + const signer = await HDWallet.random() + // Sign the valid coupons and insert them into the archivist + for (const coupon of validCoupons) { + const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() + await archivist.insert([bw, ...payloads]) + } + // Insert (but do not sign) the unsigned coupons into the archivist + await archivist.insert(unsignedCoupons) + const boundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ + account: 'random', + config: { + archivist: archivist.address, + schema: MemoryBoundWitnessDiviner.defaultConfigSchema, + }, + }) + sut = await PaymentDiscountDiviner.create({ + account: 'random', + config: { + archivist: archivist.address, + boundWitnessDiviner: boundWitnessDiviner.address, + couponAuthorities: [signer.address], + schema: PaymentDiscountDivinerConfigSchema, + }, + }) + const modules = [archivist, boundWitnessDiviner, sut] + for (const mod of modules) { + await node.register(mod) + await node.attach(mod.address, false) + } + }) + beforeEach(() => { + vi.clearAllMocks() + }) + describe('with valid coupon', () => { + it.each(validCoupons)('Applies coupon', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 10, schema: DiscountSchema }) + }) + }) + describe('with invalid coupon', () => { + it.each(unsignedCoupons)('Does not apply coupons', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) + }) + }) + describe('with expired coupon', () => { + const now = Date.now() + const expiredCoupons: Coupon[] = [ + // In past + { + amount: 10, exp: now, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + // In future + { + percentage: 0.1, exp: now + 100_000_000, nbf: now + 10_000_000, schema: FixedPercentageCouponSchema, + }, + ] + it.each(expiredCoupons)('Does not apply coupons', async (coupon) => { + const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } + const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isDiscount) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) + }) + }) +}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts new file mode 100644 index 000000000..42b7e9050 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts @@ -0,0 +1,20 @@ +import type { Payment } from '@xyo-network/payment-payload-plugins' + +import type { Discount } from '../Discount/index.ts' +import type { Subtotal } from '../Subtotal/index.ts' +import type { Total } from '../Total/index.ts' + +/** + * A tuple containing the subtotal, total, and payment for an invoice. + */ +export type StandardInvoice = [Subtotal, Total, Payment] + +/** + * A tuple containing the subtotal, total, payment, and discount for an invoice. + */ +export type DiscountedInvoice = [...StandardInvoice, Discount] + +/** + * An invoice. + */ +export type Invoice = StandardInvoice | DiscountedInvoice diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts new file mode 100644 index 000000000..2329fe071 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts @@ -0,0 +1,43 @@ +import type { Hash } from '@xylabs/hex' +import type { DivinerInstance } from '@xyo-network/diviner-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { Payload } from '@xyo-network/payload-model' +import type { EscrowTerms, Payment } from '@xyo-network/payment-payload-plugins' +import { PaymentSchema } from '@xyo-network/payment-payload-plugins' + +import type { Discount } from '../Discount/index.ts' +import { isDiscount } from '../Discount/index.ts' +import type { Subtotal } from '../Subtotal/index.ts' +import { isSubtotal } from '../Subtotal/index.ts' +import type { Total } from '../Total/index.ts' +import { isTotal } from '../Total/index.ts' +import type { Invoice } from './Invoice.ts' + +/** + * Validates the escrow terms to ensure they are valid for a purchase + * @returns A payment if the terms are valid for a purchase, undefined otherwise + */ +export const getInvoiceForEscrow = async ( + terms: EscrowTerms, + dataHashMap: Record, + paymentTotalDiviner: DivinerInstance, +): Promise => { + const payloads = Object.values(dataHashMap) + const results = await paymentTotalDiviner.divine([terms, ...payloads]) + const subtotal = results.find(isSubtotal) as Subtotal | undefined + const discount = results.find(isDiscount) as Discount | undefined + const total = results.find(isTotal) as Total | undefined + if (!subtotal || !total) return undefined + const { amount, currency } = total + if (currency !== 'USD') return undefined + const sources = await getSources(terms, subtotal, total, discount) + const payment: Payment = { + amount, currency, schema: PaymentSchema, sources, + } + return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment] +} + +const getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise => { + const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total] + return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p))) +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts new file mode 100644 index 000000000..2878663f2 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts @@ -0,0 +1,2 @@ +export * from './getInvoiceForEscrow.ts' +export * from './Invoice.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts new file mode 100644 index 000000000..04d2059c4 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts @@ -0,0 +1,69 @@ +import { assertEx } from '@xylabs/assert' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' + +import { NO_DISCOUNT, PaymentDiscountDiviner } from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' +import type { PaymentTotalDivinerConfig } from '../../Total/index.ts' +import { PaymentTotalDiviner } from '../../Total/index.ts' +import { getInvoiceForEscrow } from '../getInvoiceForEscrow.ts' + +describe('getInvoiceForEscrow', () => { + let node: MemoryNode + let paymentDiscountDiviner: PaymentDiscountDiviner + let paymentSubtotalDiviner: PaymentSubtotalDiviner + let paymentTotalDiviner: PaymentTotalDiviner + beforeAll(async () => { + node = await MemoryNode.create({ account: 'random' }) + paymentDiscountDiviner = await PaymentDiscountDiviner.create({ account: 'random' }) + paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) + const config: PaymentTotalDivinerConfig = { + paymentDiscountDiviner: paymentDiscountDiviner.address, + paymentSubtotalDiviner: paymentSubtotalDiviner.address, + schema: PaymentTotalDiviner.defaultConfigSchema, + } + paymentTotalDiviner = await PaymentTotalDiviner.create({ account: 'random', config }) + const modules = [paymentDiscountDiviner, paymentSubtotalDiviner, paymentTotalDiviner] + for (const module of modules) { + await node.register(module) + await node.attach(module.address, true) + } + }) + describe('with no discount', () => { + it('should return invoice values', async () => { + const nbf = Date.now() + const exp = nbf + 1000 * 60 * 10 + const appraisal: HashLeaseEstimate = { + price: 10, currency: 'USD', schema: HashLeaseEstimateSchema, exp, nbf, + } + const appraisalHash = await PayloadBuilder.dataHash(appraisal) + const terms: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [appraisalHash], exp, nbf, + } + const dataHashMap = await PayloadBuilder.toDataHashMap([appraisal]) + const result = await getInvoiceForEscrow(terms, dataHashMap, paymentTotalDiviner) + expect(result).toBeArray() + expect(result?.length).toBeGreaterThan(0) + const invoice = assertEx(result) + const [subtotal, total, payment, discount] = invoice + expect(subtotal).toBeDefined() + expect(subtotal.amount).toBeNumber() + expect(subtotal.amount).toBe(appraisal.price) + expect(subtotal.currency).toBe('USD') + expect(total).toBeDefined() + expect(total.amount).toBeNumber() + expect(total.amount).toBe(subtotal.amount) + expect(total.currency).toBe('USD') + expect(payment).toBeDefined() + expect(payment.amount).toBeNumber() + expect(payment.amount).toBe(total.amount) + expect(payment.currency).toBe('USD') + expect(discount).toBeDefined() + expect(discount).toMatchObject(NO_DISCOUNT) + }) + }) +}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts new file mode 100644 index 000000000..8eb263689 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts @@ -0,0 +1,13 @@ +// import type { Hash } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' + +export const PaymentSubtotalDivinerConfigSchema = 'network.xyo.diviner.payments.subtotal.config' +export type PaymentSubtotalDivinerConfigSchema = typeof PaymentSubtotalDivinerConfigSchema + +/** + * The configuration for the Coupon Subtotal Diviner + */ +export type PaymentSubtotalDivinerConfig = DivinerConfig< + {}, + PaymentSubtotalDivinerConfigSchema +> diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts new file mode 100644 index 000000000..8491c1d9e --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts @@ -0,0 +1,66 @@ +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import { Payload } from '@xyo-network/payload-model' +import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { PaymentSubtotalDivinerConfigSchema } from './Config.ts' +import { + appraisalValidators, termsValidators, ValidEscrowTerms, +} from './lib/index.ts' +import { PaymentSubtotalDivinerParams } from './Params.ts' +import { Subtotal, SubtotalSchema } from './Payload.ts' + +const currency = 'USD' + +/** + * Escrow terms that contain all the valid fields for calculating a subtotal + */ +export type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload + +@creatableModule() +export class PaymentSubtotalDiviner< + TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams, + TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType, + TOut extends Subtotal = Subtotal, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentSubtotalDivinerConfigSchema] + static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema + + protected async divineHandler(payloads: TIn[] = []): Promise { + // Find the escrow terms + const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined + if (!terms) return [] + + // Run all terms validations + if (!termsValidators.every(validator => validator(terms))) return [] + const validTerms = terms as ValidEscrowTerms + + // Retrieve all appraisals from terms + const hashMap = await PayloadBuilder.toAllHashMap(payloads) + const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] + + // Ensure all appraisals are present + if (appraisals.length !== validTerms.appraisals.length) return [] + + // Run all appraisal validations + if (!appraisalValidators.every(validator => validator(appraisals))) return [] + const amount = calculateSubtotal(appraisals) + const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals] + return [{ + amount, currency, schema: SubtotalSchema, sources, + }] as TOut[] + } +} + +// TODO: Add support for other currencies +const calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => { + return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0) +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts new file mode 100644 index 000000000..7e05fbc3a --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts @@ -0,0 +1,8 @@ +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentSubtotalDivinerConfig } from './Config.ts' + +export type PaymentSubtotalDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts new file mode 100644 index 000000000..cce562286 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../Amount/index.ts' + +export const SubtotalSchema = 'network.xyo.payments.subtotal' as const +export type SubtotalSchema = typeof SubtotalSchema + +export interface SubtotalFields extends AmountFields {} + +/** + * The result of a subtotal + */ +export type Subtotal = PayloadWithSources + +/** + * Identity function for determining if an object is an Subtotal + */ +export const isSubtotal = isPayloadOfSchemaType(SubtotalSchema) + +/** + * Identity function for determining if an object is an Subtotal with sources + */ +export const isSubtotalWithSources = isPayloadOfSchemaTypeWithSources(SubtotalSchema) + +/** + * Identity function for determining if an object is an Subtotal with meta + */ +export const isSubtotalWithMeta = isPayloadOfSchemaTypeWithMeta(SubtotalSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts new file mode 100644 index 000000000..7a0ddb968 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts @@ -0,0 +1,4 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './Params.ts' +export * from './Payload.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts new file mode 100644 index 000000000..94f009b80 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts @@ -0,0 +1,45 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' + +import { isIso4217CurrencyCode } from '../../Amount/index.ts' +import { validateDuration } from './durationValidators.ts' + +const validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => { + // Ensure all appraisals are numeric + if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false + // Ensure all appraisals are positive numbers + if (appraisals.some(appraisal => appraisal.price < 0)) return false + return true +} + +const validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => { + // NOTE: Only supporting USD for now, the remaining checks are for future-proofing. + if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false + + // Check every object in the array to ensure they all are in a supported currency. + if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false + + return true +} + +const validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => { + // Check if the array is empty or contains only one element, no need to compare. + if (appraisals.length <= 1) return true + + // Get the currency of the first element to compare with others. + const { currency } = appraisals[0] + if (!currency) return false + + // Check every object in the array to ensure they all have the same currency. + if (!appraisals.every(item => item.currency === currency)) return false + + return true +} + +const validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration) + +export const appraisalValidators = [ + validateAppraisalAmount, + validateAppraisalCurrency, + validateAppraisalConsistentCurrency, + validateAppraisalWindow, +] diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts new file mode 100644 index 000000000..52831e574 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts @@ -0,0 +1,18 @@ +import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' + +const FIVE_MINUTES = 1000 * 60 * 5 + +/** + * Validates that the current time is within the duration window, within a configurable a buffer + * @param value The duration value + * @param windowMs The window in milliseconds to allow for a buffer + * @returns True if the duration is valid, false otherwise + */ +export const validateDuration = (value: Partial, windowMs = FIVE_MINUTES): boolean => { + const now = Date.now() + if (!value.nbf || value.nbf > now) return false + // If already expired (include for a 5 minute buffer to allow for a reasonable + // minimum amount of time for the transaction to be processed) + if (!value.exp || value.exp - now < windowMs) return false + return true +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts new file mode 100644 index 000000000..c454d02eb --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './appraisalValidators.ts' +export * from './termsValidators.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts new file mode 100644 index 000000000..c447a824b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts @@ -0,0 +1,18 @@ +import type { Hash } from '@xylabs/hex' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' + +import { validateDuration } from './durationValidators.ts' + +export type ValidEscrowTerms = Required + +const validateTermsAppraisals = (terms: EscrowTerms): terms is Required => { + if (!terms.appraisals) return false + if (terms.appraisals.length === 0) return false + return true +} +const validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms) + +export const termsValidators = [ + validateTermsAppraisals, + validateTermsWindow, +] diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts new file mode 100644 index 000000000..b31c90e33 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts @@ -0,0 +1,110 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeAll, beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentSubtotalDiviner } from '../Diviner.ts' +import { + isSubtotal, + SubtotalSchema, +} from '../Payload.ts' + +describe('PaymentSubtotalDiviner', () => { + let sut: PaymentSubtotalDiviner + const nbf = Date.now() + const exp = Number.MAX_SAFE_INTEGER + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ + [ + [ + { + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + ], 11], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, + }, + ], 30], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + ], 300], + ] + beforeEach(() => { + vi.clearAllMocks() + }) + beforeAll(async () => { + sut = await PaymentSubtotalDiviner.create({ account: 'random' }) + }) + describe('with escrow terms containing valid appraisals', () => { + it.each(cases)('calculates the subtotal of all the appraisals', async (appraisals, total) => { + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(1) + const result = results.find(isSubtotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: SubtotalSchema }) + expect(result?.sources).toEqual([await PayloadBuilder.dataHash(terms), ...appraisalHashes]) + }) + }) + describe('with escrow terms containing invalid appraisals', () => { + describe('when containing negative values in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: -1, currency: 'USD', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + describe('when containing non-numeric values in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: 'three dollars', currency: 'USD', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + describe('when containing mixed currencies in appraisals', () => { + it('calculates the subtotal of all the appraisals', async () => { + const appraisals = [{ + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, { + schema: HashLeaseEstimateSchema, price: 1, currency: 'EUR', exp, nbf, + }] + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(0) + }) + }) + }) +}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts new file mode 100644 index 000000000..143770b1a --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts @@ -0,0 +1,24 @@ +// import type { Hash } from '@xylabs/hex' +import type { DivinerConfig } from '@xyo-network/diviner-model' +import type { ModuleIdentifier } from '@xyo-network/module-model' + +export const PaymentTotalDivinerConfigSchema = 'network.xyo.diviner.payments.total.config' +export type PaymentTotalDivinerConfigSchema = typeof PaymentTotalDivinerConfigSchema + +/** + * The configuration for the Total Diviner + */ +export type PaymentTotalDivinerConfig = DivinerConfig< + { + /** + * The Diviner that will be used to determine the discount + */ + paymentDiscountDiviner?: ModuleIdentifier + + /** + * The Diviner that will be used to determine the subtotal + */ + paymentSubtotalDiviner?: ModuleIdentifier + }, + PaymentTotalDivinerConfigSchema +> diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts new file mode 100644 index 000000000..9db4086c6 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts @@ -0,0 +1,70 @@ +import { assertEx } from '@xylabs/assert' +import { Hash } from '@xylabs/hex' +import { AbstractDiviner } from '@xyo-network/diviner-abstract' +import { + asDivinerInstance, DivinerInstance, DivinerModuleEventData, +} from '@xyo-network/diviner-model' +import { creatableModule } from '@xyo-network/module-model' + +import { + Discount, + isDiscount, PaymentDiscountDiviner, + PaymentDiscountDivinerInputType, +} from '../Discount/index.ts' +import { + isSubtotal, PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType, Subtotal, +} from '../Subtotal/index.ts' +import { PaymentTotalDivinerConfigSchema } from './Config.ts' +import { PaymentTotalDivinerParams } from './Params.ts' +import { Total, TotalSchema } from './Payload.ts' + +type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType +type OutputType = Subtotal | Discount | Total + +@creatableModule() +export class PaymentTotalDiviner< + TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams, + TIn extends InputType = InputType, + TOut extends OutputType = OutputType, + TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< + DivinerInstance, + TIn, + TOut + >, +> extends AbstractDiviner { + static override configSchemas = [PaymentTotalDivinerConfigSchema] + static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema + + protected async divineHandler(payloads: TIn[] = []): Promise { + const subtotalDiviner = await this.getPaymentSubtotalDiviner() + const subtotalResult = await subtotalDiviner.divine(payloads) + const subtotal = subtotalResult.find(isSubtotal) + if (!subtotal) return [] + const discountDiviner = await this.getPaymentDiscountsDiviner() + const discountResult = await discountDiviner.divine(payloads) + const discount = discountResult.find(isDiscount) + if (!discount) return [] + const { currency: subtotalCurrency } = subtotal + const { currency: discountCurrency } = discount + assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`) + const amount = Math.max(0, subtotal.amount - discount.amount) + const currency = subtotalCurrency + const sources = [subtotal.$hash, discount.$hash] as Hash[] + const total: Total = { + amount, currency, sources, schema: TotalSchema, + } + return [subtotal, discount, total] as TOut[] + } + + protected async getPaymentDiscountsDiviner(): Promise { + const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner + } + + protected async getPaymentSubtotalDiviner(): Promise { + const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config') + const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`) + return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner + } +} diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts new file mode 100644 index 000000000..07942f3ba --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts @@ -0,0 +1,8 @@ +import type { DivinerParams } from '@xyo-network/diviner-model' +import type { AnyConfigSchema } from '@xyo-network/module-model' + +import type { PaymentTotalDivinerConfig } from './Config.ts' + +export type PaymentTotalDivinerParams< + TConfig extends AnyConfigSchema = AnyConfigSchema, +> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts new file mode 100644 index 000000000..0e9e073f1 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts @@ -0,0 +1,33 @@ +import type { PayloadWithSources } from '@xyo-network/payload-model' +import { + isPayloadOfSchemaType, + isPayloadOfSchemaTypeWithMeta, + isPayloadOfSchemaTypeWithSources, +} from '@xyo-network/payload-model' + +import type { AmountFields } from '../Amount/index.ts' + +export const TotalSchema = 'network.xyo.payments.total' as const +export type TotalSchema = typeof TotalSchema + +export interface TotalFields extends AmountFields {} + +/** + * The result of a total + */ +export type Total = PayloadWithSources + +/** + * Identity function for determining if an object is an Total + */ +export const isTotal = isPayloadOfSchemaType(TotalSchema) + +/** + * Identity function for determining if an object is an Total with sources + */ +export const isTotalWithSources = isPayloadOfSchemaTypeWithSources(TotalSchema) + +/** + * Identity function for determining if an object is an Total with meta + */ +export const isTotalWithMeta = isPayloadOfSchemaTypeWithMeta(TotalSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/index.ts b/packages/payloadset/packages/payments/src/Checkout/Total/index.ts new file mode 100644 index 000000000..7a0ddb968 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/index.ts @@ -0,0 +1,4 @@ +export * from './Config.ts' +export * from './Diviner.ts' +export * from './Params.ts' +export * from './Payload.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts new file mode 100644 index 000000000..dbbce2ac6 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts @@ -0,0 +1,173 @@ +import { HDWallet } from '@xyo-network/account' +import { MemoryArchivist } from '@xyo-network/archivist-memory' +import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' +import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import { MemoryNode } from '@xyo-network/node-memory' +import { PayloadBuilder } from '@xyo-network/payload-builder' +import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + beforeAll, beforeEach, describe, it, vi, +} from 'vitest' + +import type { Coupon } from '../../Discount/index.ts' +import { + FixedAmountCouponSchema, FixedPercentageCouponSchema, PaymentDiscountDiviner, +} from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' +import { PaymentTotalDiviner } from '../Diviner.ts' +import { + isTotal, + TotalSchema, +} from '../Payload.ts' + +describe('PaymentTotalDiviner', () => { + let sut: PaymentTotalDiviner + const nbf = Date.now() + const exp = nbf + 1000 * 60 * 10 + const termsBase: EscrowTerms = { + schema: EscrowTermsSchema, appraisals: [], exp, nbf, + } + const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ + [ + [ + { + schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + ], 11], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, + }, + ], 30], + [ + [ + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + { + schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, + }, + ], 300], + ] + const validCoupons: Coupon[] = [ + { + amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, + }, + ] + const unsignedCoupons: Coupon[] = [ + { + amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', + }, + { + percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, + }, + ] + beforeEach(() => { + vi.clearAllMocks() + }) + beforeAll(async () => { + const node = await MemoryNode.create({ account: 'random' }) + expect(node).toBeDefined() + const paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) + const discountsArchivist = await MemoryArchivist.create({ account: 'random' }) + const signer = await HDWallet.random() + // Sign the valid coupons and insert them into the archivist + for (const coupon of validCoupons) { + const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() + await discountsArchivist.insert([bw, ...payloads]) + } + // Insert (but do not sign) the unsigned coupons into the archivist + await discountsArchivist.insert(unsignedCoupons) + const discountsBoundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ + account: 'random', + config: { + archivist: discountsArchivist.address, + schema: MemoryBoundWitnessDiviner.defaultConfigSchema, + }, + }) + const discountDiviner = await PaymentDiscountDiviner.create({ + account: 'random', + config: { + archivist: discountsArchivist.address, + boundWitnessDiviner: discountsBoundWitnessDiviner.address, + couponAuthorities: [signer.address], + schema: PaymentDiscountDiviner.defaultConfigSchema, + }, + }) + sut = await PaymentTotalDiviner.create({ + account: 'random', + config: { + paymentDiscountDiviner: discountDiviner.address, + paymentSubtotalDiviner: paymentSubtotalDiviner.address, + schema: PaymentTotalDiviner.defaultConfigSchema, + }, + }) + + const modules = [paymentSubtotalDiviner, discountsArchivist, discountsBoundWitnessDiviner, discountDiviner, sut] + for (const mod of modules) { + await node.register(mod) + await node.attach(mod.address, true) + } + }) + describe('with valid escrow', () => { + describe('with no discounts', () => { + it.each(cases)('calculates total', async (appraisals, total) => { + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } + const results = await sut.divine([terms, ...appraisals]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: TotalSchema }) + }) + }) + describe('with valid discounts', () => { + describe.each(cases)('calculates total', (appraisals, total) => { + it.each(validCoupons)('applying coupon discount to total', async (coupon) => { + const discounts = await PayloadBuilder.dataHashes([coupon]) + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { + ...termsBase, appraisals: appraisalHashes, discounts, + } + const results = await sut.divine([terms, ...appraisals, coupon]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result?.amount).toBeLessThan(total) + }) + }) + }) + describe('with invalid discounts', () => { + describe.each(cases)('calculates total', (appraisals, total) => { + it.each(unsignedCoupons)('without applying coupon discount to total', async (coupon) => { + const discounts = await PayloadBuilder.dataHashes([coupon]) + const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) + const terms: EscrowTerms = { + ...termsBase, appraisals: appraisalHashes, discounts, + } + const results = await sut.divine([terms, ...appraisals, coupon]) + expect(results).toBeArrayOfSize(3) + const result = results.find(isTotal) + expect(result).toBeDefined() + expect(result).toMatchObject({ amount: total, schema: TotalSchema }) + }) + }) + }) + }) +}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/index.ts b/packages/payloadset/packages/payments/src/Checkout/index.ts new file mode 100644 index 000000000..16d47c683 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Checkout/index.ts @@ -0,0 +1,5 @@ +export * from './Amount/index.ts' +export * from './Discount/index.ts' +export * from './Invoice/index.ts' +export * from './Subtotal/index.ts' +export * from './Total/index.ts' diff --git a/packages/payloadset/packages/payments/src/index.ts b/packages/payloadset/packages/payments/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/payloadset/packages/payments/tsconfig.json b/packages/payloadset/packages/payments/tsconfig.json new file mode 100644 index 000000000..2acc1cf58 --- /dev/null +++ b/packages/payloadset/packages/payments/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@xylabs/tsconfig" +} + diff --git a/packages/payloadset/packages/payments/tsconfig.typedoc.json b/packages/payloadset/packages/payments/tsconfig.typedoc.json new file mode 100644 index 000000000..0919ecb67 --- /dev/null +++ b/packages/payloadset/packages/payments/tsconfig.typedoc.json @@ -0,0 +1,5 @@ +{ + "exclude": ["**/spec/*", "**/*.spec.*", "**/packages/*", "dist"], + "extends": "./tsconfig.json" +} + diff --git a/packages/payloadset/packages/payments/typedoc.json b/packages/payloadset/packages/payments/typedoc.json new file mode 100644 index 000000000..a1ff9f91c --- /dev/null +++ b/packages/payloadset/packages/payments/typedoc.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "tsconfig": "./tsconfig.typedoc.json" +} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/xy.config.ts b/packages/payloadset/packages/payments/xy.config.ts new file mode 100644 index 000000000..887554345 --- /dev/null +++ b/packages/payloadset/packages/payments/xy.config.ts @@ -0,0 +1,11 @@ +import type { XyTsupConfig } from '@xylabs/ts-scripts-yarn3' +const config: XyTsupConfig = { + compile: { + browser: {}, + node: {}, + neutral: { src: true }, + }, +} + +// eslint-disable-next-line import-x/no-default-export +export default config From e1fbe969a734328377e61f121cb39a4cde1883d9 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 19:46:28 -0500 Subject: [PATCH 2/9] Refactor only types in payload plugins --- .../payload/packages/payments/package.json | 1 + .../packages/payments/src/Invoice/Invoice.ts | 3 +- .../src/Invoice/getInvoiceForEscrow.ts | 43 -- .../src/Subtotal/{ => Diviner}/Config.ts | 0 .../src/Subtotal/{ => Diviner}/Params.ts | 0 .../src/Subtotal/{ => Diviner}/Payload.ts | 2 +- .../payments/src/Subtotal/Diviner}/index.ts | 1 - .../packages/payments/src/Subtotal/index.ts | 5 +- .../src/Total/{ => Diviner}/Config.ts | 0 .../src/Total/{ => Diviner}/Params.ts | 0 .../payments/src/Total/Diviner}/Payload.ts | 2 +- .../payments/src/Total/Diviner}/index.ts | 0 .../packages/payments/src/Total/Payload.ts | 33 -- .../packages/payments/src/Total/index.ts | 5 +- .../payloadset/packages/payments/package.json | 2 +- .../src/Checkout/Amount/Iso4217Currency.ts | 557 ------------------ .../payments/src/Checkout/Amount/Payload.ts | 36 -- .../payments/src/Checkout/Amount/index.ts | 2 - .../payments/src/Checkout/Discount/Config.ts | 34 -- .../payments/src/Checkout/Discount/Params.ts | 14 - .../Payload/Coupon/Coupons/FixedAmount.ts | 37 -- .../Payload/Coupon/Coupons/FixedPercentage.ts | 37 -- .../Discount/Payload/Coupon/Coupons/index.ts | 2 - .../Discount/Payload/Coupon/Payload.ts | 25 - .../Discount/Payload/Coupon/Schema.ts | 2 - .../Checkout/Discount/Payload/Coupon/index.ts | 4 - .../Payload/Coupon/types/CouponFields.ts | 11 - .../Discount/Payload/Coupon/types/index.ts | 2 - .../Payload/Coupon/types/isStackable.ts | 6 - .../src/Checkout/Discount/Payload/Discount.ts | 33 -- .../Checkout/Discount/Payload/NoDiscount.ts | 8 - .../src/Checkout/Discount/Payload/index.ts | 3 - .../payments/src/Checkout/Discount/index.ts | 5 - .../payments/src/Checkout/Invoice/Invoice.ts | 20 - .../Invoice/spec/getInvoiceForEscrow.spec.ts | 69 --- .../src/Checkout/Invoice/spec/tsconfig.json | 3 - .../payments/src/Checkout/Subtotal/Config.ts | 13 - .../payments/src/Checkout/Subtotal/Diviner.ts | 66 --- .../payments/src/Checkout/Subtotal/Params.ts | 8 - .../payments/src/Checkout/Subtotal/Payload.ts | 33 -- .../Subtotal/lib/appraisalValidators.ts | 45 -- .../Subtotal/lib/durationValidators.ts | 18 - .../src/Checkout/Subtotal/lib/index.ts | 2 - .../Checkout/Subtotal/lib/termsValidators.ts | 18 - .../Checkout/Subtotal/spec/Diviner.spec.ts | 110 ---- .../src/Checkout/Subtotal/spec/tsconfig.json | 3 - .../payments/src/Checkout/Total/Config.ts | 24 - .../payments/src/Checkout/Total/Diviner.ts | 70 --- .../payments/src/Checkout/Total/Params.ts | 8 - .../src/Checkout/Total/spec/Diviner.spec.ts | 173 ------ .../src/Checkout/Total/spec/tsconfig.json | 3 - .../packages/payments/src/Checkout/index.ts | 5 - .../src/{Checkout => }/Discount/Diviner.ts | 0 .../packages/payments/src/Discount/index.ts | 2 + .../Discount/lib/applyCoupons.ts | 0 .../src/{Checkout => }/Discount/lib/index.ts | 0 .../Discount/lib/spec/applyCoupons.spec.ts | 0 .../src/Discount/lib}/spec/tsconfig.json | 0 .../Discount/spec/Diviner.spec.ts | 0 .../payments/src/Discount}/spec/tsconfig.json | 0 .../Invoice/getInvoiceForEscrow.ts | 0 .../src/{Checkout => }/Invoice/index.ts | 0 .../Invoice/spec/getInvoiceForEscrow.spec.ts | 0 .../payments/src/Invoice}/spec/tsconfig.json | 0 .../packages/payments/src/Subtotal/Diviner.ts | 0 .../packages/payments/src/Subtotal/index.ts | 1 + .../src/Subtotal/lib/appraisalValidators.ts | 0 .../src/Subtotal/lib/durationValidators.ts | 0 .../payments/src/Subtotal/lib/index.ts | 0 .../src/Subtotal/lib/termsValidators.ts | 0 .../src/Subtotal/spec/Diviner.spec.ts | 0 .../lib => Subtotal}/spec/tsconfig.json | 0 .../packages/payments/src/Total/Diviner.ts | 0 .../packages/payments/src/Total/index.ts | 1 + .../payments/src/Total/spec/Diviner.spec.ts | 0 .../Discount => Total}/spec/tsconfig.json | 0 .../payloadset/packages/payments/src/index.ts | 4 + yarn.lock | 24 + 78 files changed, 39 insertions(+), 1599 deletions(-) delete mode 100644 packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts rename packages/payload/packages/payments/src/Subtotal/{ => Diviner}/Config.ts (100%) rename packages/payload/packages/payments/src/Subtotal/{ => Diviner}/Params.ts (100%) rename packages/payload/packages/payments/src/Subtotal/{ => Diviner}/Payload.ts (94%) rename packages/{payloadset/packages/payments/src/Checkout/Total => payload/packages/payments/src/Subtotal/Diviner}/index.ts (74%) rename packages/payload/packages/payments/src/Total/{ => Diviner}/Config.ts (100%) rename packages/payload/packages/payments/src/Total/{ => Diviner}/Params.ts (100%) rename packages/{payloadset/packages/payments/src/Checkout/Total => payload/packages/payments/src/Total/Diviner}/Payload.ts (94%) rename packages/{payloadset/packages/payments/src/Checkout/Subtotal => payload/packages/payments/src/Total/Diviner}/index.ts (100%) delete mode 100644 packages/payload/packages/payments/src/Total/Payload.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Amount/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Discount/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Config.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/Params.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts delete mode 100644 packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json delete mode 100644 packages/payloadset/packages/payments/src/Checkout/index.ts rename packages/payloadset/packages/payments/src/{Checkout => }/Discount/Diviner.ts (100%) create mode 100644 packages/payloadset/packages/payments/src/Discount/index.ts rename packages/payloadset/packages/payments/src/{Checkout => }/Discount/lib/applyCoupons.ts (100%) rename packages/payloadset/packages/payments/src/{Checkout => }/Discount/lib/index.ts (100%) rename packages/payloadset/packages/payments/src/{Checkout => }/Discount/lib/spec/applyCoupons.spec.ts (100%) rename packages/{payload/packages/payments/src/Invoice => payloadset/packages/payments/src/Discount/lib}/spec/tsconfig.json (100%) rename packages/payloadset/packages/payments/src/{Checkout => }/Discount/spec/Diviner.spec.ts (100%) rename packages/{payload/packages/payments/src/Subtotal => payloadset/packages/payments/src/Discount}/spec/tsconfig.json (100%) rename packages/payloadset/packages/payments/src/{Checkout => }/Invoice/getInvoiceForEscrow.ts (100%) rename packages/payloadset/packages/payments/src/{Checkout => }/Invoice/index.ts (100%) rename packages/{payload => payloadset}/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts (100%) rename packages/{payload/packages/payments/src/Total => payloadset/packages/payments/src/Invoice}/spec/tsconfig.json (100%) rename packages/{payload => payloadset}/packages/payments/src/Subtotal/Diviner.ts (100%) create mode 100644 packages/payloadset/packages/payments/src/Subtotal/index.ts rename packages/{payload => payloadset}/packages/payments/src/Subtotal/lib/appraisalValidators.ts (100%) rename packages/{payload => payloadset}/packages/payments/src/Subtotal/lib/durationValidators.ts (100%) rename packages/{payload => payloadset}/packages/payments/src/Subtotal/lib/index.ts (100%) rename packages/{payload => payloadset}/packages/payments/src/Subtotal/lib/termsValidators.ts (100%) rename packages/{payload => payloadset}/packages/payments/src/Subtotal/spec/Diviner.spec.ts (100%) rename packages/payloadset/packages/payments/src/{Checkout/Discount/lib => Subtotal}/spec/tsconfig.json (100%) rename packages/{payload => payloadset}/packages/payments/src/Total/Diviner.ts (100%) create mode 100644 packages/payloadset/packages/payments/src/Total/index.ts rename packages/{payload => payloadset}/packages/payments/src/Total/spec/Diviner.spec.ts (100%) rename packages/payloadset/packages/payments/src/{Checkout/Discount => Total}/spec/tsconfig.json (100%) diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index 37767b314..b71964720 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -37,6 +37,7 @@ "@xyo-network/boundwitness-model": "^3.1.9", "@xyo-network/boundwitness-validator": "^3.1.9", "@xyo-network/diviner-hash-lease": "^3.1.9", + "@xyo-network/diviner-model": "^3.1.9", "@xyo-network/id-payload-plugin": "^3.1.9", "@xyo-network/module-model": "^3.1.9", "@xyo-network/payload-model": "^3.1.9" diff --git a/packages/payload/packages/payments/src/Invoice/Invoice.ts b/packages/payload/packages/payments/src/Invoice/Invoice.ts index 42b7e9050..d07acd589 100644 --- a/packages/payload/packages/payments/src/Invoice/Invoice.ts +++ b/packages/payload/packages/payments/src/Invoice/Invoice.ts @@ -1,6 +1,5 @@ -import type { Payment } from '@xyo-network/payment-payload-plugins' - import type { Discount } from '../Discount/index.ts' +import type { Payment } from '../Payment/index.ts' import type { Subtotal } from '../Subtotal/index.ts' import type { Total } from '../Total/index.ts' diff --git a/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts b/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts deleted file mode 100644 index b4eba57bd..000000000 --- a/packages/payload/packages/payments/src/Invoice/getInvoiceForEscrow.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Hash } from '@xylabs/hex' -import type { DivinerInstance } from '@xyo-network/diviner-model' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { Payload } from '@xyo-network/payload-model' - -import type { Discount } from '../Discount/index.ts' -import { isDiscount } from '../Discount/index.ts' -import type { EscrowTerms } from '../Escrow/index.ts' -import { type Payment, PaymentSchema } from '../Payment/index.ts' -import type { Subtotal } from '../Subtotal/index.ts' -import { isSubtotal } from '../Subtotal/index.ts' -import type { Total } from '../Total/index.ts' -import { isTotal } from '../Total/index.ts' -import type { Invoice } from './Invoice.ts' - -/** - * Validates the escrow terms to ensure they are valid for a purchase - * @returns A payment if the terms are valid for a purchase, undefined otherwise - */ -export const getInvoiceForEscrow = async ( - terms: EscrowTerms, - dataHashMap: Record, - paymentTotalDiviner: DivinerInstance, -): Promise => { - const payloads = Object.values(dataHashMap) - const results = await paymentTotalDiviner.divine([terms, ...payloads]) - const subtotal = results.find(isSubtotal) as Subtotal | undefined - const discount = results.find(isDiscount) as Discount | undefined - const total = results.find(isTotal) as Total | undefined - if (!subtotal || !total) return undefined - const { amount, currency } = total - if (currency !== 'USD') return undefined - const sources = await getSources(terms, subtotal, total, discount) - const payment: Payment = { - amount, currency, schema: PaymentSchema, sources, - } - return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment] -} - -const getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise => { - const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total] - return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p))) -} diff --git a/packages/payload/packages/payments/src/Subtotal/Config.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Config.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/Config.ts rename to packages/payload/packages/payments/src/Subtotal/Diviner/Config.ts diff --git a/packages/payload/packages/payments/src/Subtotal/Params.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Params.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/Params.ts rename to packages/payload/packages/payments/src/Subtotal/Diviner/Params.ts diff --git a/packages/payload/packages/payments/src/Subtotal/Payload.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Payload.ts similarity index 94% rename from packages/payload/packages/payments/src/Subtotal/Payload.ts rename to packages/payload/packages/payments/src/Subtotal/Diviner/Payload.ts index cce562286..5703513b6 100644 --- a/packages/payload/packages/payments/src/Subtotal/Payload.ts +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/Payload.ts @@ -5,7 +5,7 @@ import { isPayloadOfSchemaTypeWithSources, } from '@xyo-network/payload-model' -import type { AmountFields } from '../Amount/index.ts' +import type { AmountFields } from '../../Amount/index.ts' export const SubtotalSchema = 'network.xyo.payments.subtotal' as const export type SubtotalSchema = typeof SubtotalSchema diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/index.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/index.ts similarity index 74% rename from packages/payloadset/packages/payments/src/Checkout/Total/index.ts rename to packages/payload/packages/payments/src/Subtotal/Diviner/index.ts index 7a0ddb968..ccda19e2f 100644 --- a/packages/payloadset/packages/payments/src/Checkout/Total/index.ts +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/index.ts @@ -1,4 +1,3 @@ export * from './Config.ts' -export * from './Diviner.ts' export * from './Params.ts' export * from './Payload.ts' diff --git a/packages/payload/packages/payments/src/Subtotal/index.ts b/packages/payload/packages/payments/src/Subtotal/index.ts index 7a0ddb968..e57fdbc61 100644 --- a/packages/payload/packages/payments/src/Subtotal/index.ts +++ b/packages/payload/packages/payments/src/Subtotal/index.ts @@ -1,4 +1 @@ -export * from './Config.ts' -export * from './Diviner.ts' -export * from './Params.ts' -export * from './Payload.ts' +export * from './Diviner/index.ts' diff --git a/packages/payload/packages/payments/src/Total/Config.ts b/packages/payload/packages/payments/src/Total/Diviner/Config.ts similarity index 100% rename from packages/payload/packages/payments/src/Total/Config.ts rename to packages/payload/packages/payments/src/Total/Diviner/Config.ts diff --git a/packages/payload/packages/payments/src/Total/Params.ts b/packages/payload/packages/payments/src/Total/Diviner/Params.ts similarity index 100% rename from packages/payload/packages/payments/src/Total/Params.ts rename to packages/payload/packages/payments/src/Total/Diviner/Params.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts b/packages/payload/packages/payments/src/Total/Diviner/Payload.ts similarity index 94% rename from packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts rename to packages/payload/packages/payments/src/Total/Diviner/Payload.ts index 0e9e073f1..24304da28 100644 --- a/packages/payloadset/packages/payments/src/Checkout/Total/Payload.ts +++ b/packages/payload/packages/payments/src/Total/Diviner/Payload.ts @@ -5,7 +5,7 @@ import { isPayloadOfSchemaTypeWithSources, } from '@xyo-network/payload-model' -import type { AmountFields } from '../Amount/index.ts' +import type { AmountFields } from '../../Amount/index.ts' export const TotalSchema = 'network.xyo.payments.total' as const export type TotalSchema = typeof TotalSchema diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts b/packages/payload/packages/payments/src/Total/Diviner/index.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Subtotal/index.ts rename to packages/payload/packages/payments/src/Total/Diviner/index.ts diff --git a/packages/payload/packages/payments/src/Total/Payload.ts b/packages/payload/packages/payments/src/Total/Payload.ts deleted file mode 100644 index 0e9e073f1..000000000 --- a/packages/payload/packages/payments/src/Total/Payload.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import type { AmountFields } from '../Amount/index.ts' - -export const TotalSchema = 'network.xyo.payments.total' as const -export type TotalSchema = typeof TotalSchema - -export interface TotalFields extends AmountFields {} - -/** - * The result of a total - */ -export type Total = PayloadWithSources - -/** - * Identity function for determining if an object is an Total - */ -export const isTotal = isPayloadOfSchemaType(TotalSchema) - -/** - * Identity function for determining if an object is an Total with sources - */ -export const isTotalWithSources = isPayloadOfSchemaTypeWithSources(TotalSchema) - -/** - * Identity function for determining if an object is an Total with meta - */ -export const isTotalWithMeta = isPayloadOfSchemaTypeWithMeta(TotalSchema) diff --git a/packages/payload/packages/payments/src/Total/index.ts b/packages/payload/packages/payments/src/Total/index.ts index 7a0ddb968..e57fdbc61 100644 --- a/packages/payload/packages/payments/src/Total/index.ts +++ b/packages/payload/packages/payments/src/Total/index.ts @@ -1,4 +1 @@ -export * from './Config.ts' -export * from './Diviner.ts' -export * from './Params.ts' -export * from './Payload.ts' +export * from './Diviner/index.ts' diff --git a/packages/payloadset/packages/payments/package.json b/packages/payloadset/packages/payments/package.json index 65655c445..e89b98895 100644 --- a/packages/payloadset/packages/payments/package.json +++ b/packages/payloadset/packages/payments/package.json @@ -1,5 +1,5 @@ { - "name": "@xyo-network/payment-payloadset-plugins", + "name": "@xyo-network/payment-plugin", "version": "3.0.17", "description": "Typescript/Javascript Plugins for XYO Platform", "homepage": "https://xyo.network", diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts deleted file mode 100644 index 2344e0398..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Amount/Iso4217Currency.ts +++ /dev/null @@ -1,557 +0,0 @@ -/* eslint-disable max-lines */ -/** - * ISO 4217 currency codes - */ -export type Iso4217AlphabeticalCode = - 'AED' | - 'AFN' | - 'ALL' | - 'AMD' | - 'ANG' | - 'AOA' | - 'ARS' | - 'AUD' | - 'AWG' | - 'AZN' | - 'BAM' | - 'BBD' | - 'BDT' | - 'BGN' | - 'BHD' | - 'BIF' | - 'BMD' | - 'BND' | - 'BOB' | - 'BOV' | - 'BRL' | - 'BSD' | - 'BTN' | - 'BWP' | - 'BYN' | - 'BZD' | - 'CAD' | - 'CDF' | - 'CHE' | - 'CHF' | - 'CHW' | - 'CLF' | - 'CLP' | - 'CNY' | - 'COP' | - 'COU' | - 'CRC' | - 'CUP' | - 'CVE' | - 'CZK' | - 'DJF' | - 'DKK' | - 'DOP' | - 'DZD' | - 'EGP' | - 'ERN' | - 'ETB' | - 'EUR' | - 'FJD' | - 'FKP' | - 'GBP' | - 'GEL' | - 'GHS' | - 'GIP' | - 'GMD' | - 'GNF' | - 'GTQ' | - 'GYD' | - 'HKD' | - 'HNL' | - 'HTG' | - 'HUF' | - 'IDR' | - 'ILS' | - 'INR' | - 'IQD' | - 'IRR' | - 'ISK' | - 'JMD' | - 'JOD' | - 'JPY' | - 'KES' | - 'KGS' | - 'KHR' | - 'KMF' | - 'KPW' | - 'KRW' | - 'KWD' | - 'KYD' | - 'KZT' | - 'LAK' | - 'LBP' | - 'LKR' | - 'LRD' | - 'LSL' | - 'LYD' | - 'MAD' | - 'MDL' | - 'MGA' | - 'MKD' | - 'MMK' | - 'MNT' | - 'MOP' | - 'MRU' | - 'MUR' | - 'MVR' | - 'MWK' | - 'MXN' | - 'MXV' | - 'MYR' | - 'MZN' | - 'NAD' | - 'NGN' | - 'NIO' | - 'NOK' | - 'NPR' | - 'NZD' | - 'OMR' | - 'PAB' | - 'PEN' | - 'PGK' | - 'PHP' | - 'PKR' | - 'PLN' | - 'PYG' | - 'QAR' | - 'RON' | - 'RSD' | - 'RUB' | - 'RWF' | - 'SAR' | - 'SBD' | - 'SCR' | - 'SDG' | - 'SEK' | - 'SGD' | - 'SHP' | - 'SLE' | - 'SOS' | - 'SRD' | - 'SSP' | - 'STN' | - 'SVC' | - 'SYP' | - 'SZL' | - 'THB' | - 'TJS' | - 'TMT' | - 'TND' | - 'TOP' | - 'TRY' | - 'TTD' | - 'TWD' | - 'TZS' | - 'UAH' | - 'UGX' | - 'USD' | - 'USN' | - 'UYI' | - 'UYU' | - 'UYW' | - 'UZS' | - 'VED' | - 'VES' | - 'VND' | - 'VUV' | - 'WST' | - 'XAF' | - 'XAG' | - 'XAU' | - 'XBA' | - 'XBB' | - 'XBC' | - 'XBD' | - 'XCD' | - 'XDR' | - 'XOF' | - 'XPD' | - 'XPF' | - 'XPT' | - 'XSU' | - 'XTS' | - 'XUA' | - 'XXX' | - 'YER' | - 'ZAR' | - 'ZMW' | - 'ZWG' | - 'ZWL' - -// TODO: Technically, the values should be 3 digit numbers with leading -// zeros, so we can padStart if we need to be strict -/** - * ISO 4217 numeric currency number codes - */ -export type Iso4217NumericCode = - 784 | - 971 | - 8 | - 51 | - 532 | - 973 | - 32 | - 36 | - 533 | - 944 | - 977 | - 52 | - 50 | - 975 | - 48 | - 108 | - 60 | - 96 | - 68 | - 984 | - 986 | - 44 | - 64 | - 72 | - 933 | - 84 | - 124 | - 976 | - 947 | - 756 | - 948 | - 990 | - 152 | - 156 | - 170 | - 970 | - 188 | - 192 | - 132 | - 203 | - 262 | - 208 | - 214 | - 12 | - 818 | - 232 | - 230 | - 978 | - 242 | - 238 | - 826 | - 981 | - 936 | - 292 | - 270 | - 324 | - 320 | - 328 | - 344 | - 340 | - 332 | - 348 | - 360 | - 376 | - 356 | - 368 | - 364 | - 352 | - 388 | - 400 | - 392 | - 404 | - 417 | - 116 | - 174 | - 408 | - 410 | - 414 | - 136 | - 398 | - 418 | - 422 | - 144 | - 430 | - 426 | - 434 | - 504 | - 498 | - 969 | - 807 | - 104 | - 496 | - 446 | - 929 | - 480 | - 462 | - 454 | - 484 | - 979 | - 458 | - 943 | - 516 | - 566 | - 558 | - 578 | - 524 | - 554 | - 512 | - 590 | - 604 | - 598 | - 608 | - 586 | - 985 | - 600 | - 634 | - 946 | - 941 | - 643 | - 646 | - 682 | - 90 | - 690 | - 938 | - 752 | - 702 | - 654 | - 925 | - 706 | - 968 | - 728 | - 930 | - 222 | - 760 | - 748 | - 764 | - 972 | - 934 | - 788 | - 776 | - 949 | - 780 | - 901 | - 834 | - 980 | - 800 | - 840 | - 997 | - 940 | - 858 | - 927 | - 860 | - 926 | - 928 | - 704 | - 548 | - 882 | - 950 | - 961 | - 959 | - 955 | - 956 | - 957 | - 958 | - 951 | - 960 | - 952 | - 964 | - 953 | - 962 | - 994 | - 963 | - 965 | - 999 | - 886 | - 710 | - 967 | - 924 | - 932 - -/** - * Dictionary of ISO 4217 alphabetical currency codes to numeric currency number codes - */ -export const Iso4217CurrencyCodes: Record = { - AED: 784, - AFN: 971, - ALL: 8, - AMD: 51, - ANG: 532, - AOA: 973, - ARS: 32, - AUD: 36, - AWG: 533, - AZN: 944, - BAM: 977, - BBD: 52, - BDT: 50, - BGN: 975, - BHD: 48, - BIF: 108, - BMD: 60, - BND: 96, - BOB: 68, - BOV: 984, - BRL: 986, - BSD: 44, - BTN: 64, - BWP: 72, - BYN: 933, - BZD: 84, - CAD: 124, - CDF: 976, - CHE: 947, - CHF: 756, - CHW: 948, - CLF: 990, - CLP: 152, - CNY: 156, - COP: 170, - COU: 970, - CRC: 188, - CUP: 192, - CVE: 132, - CZK: 203, - DJF: 262, - DKK: 208, - DOP: 214, - DZD: 12, - EGP: 818, - ERN: 232, - ETB: 230, - EUR: 978, - FJD: 242, - FKP: 238, - GBP: 826, - GEL: 981, - GHS: 936, - GIP: 292, - GMD: 270, - GNF: 324, - GTQ: 320, - GYD: 328, - HKD: 344, - HNL: 340, - HTG: 332, - HUF: 348, - IDR: 360, - ILS: 376, - INR: 356, - IQD: 368, - IRR: 364, - ISK: 352, - JMD: 388, - JOD: 400, - JPY: 392, - KES: 404, - KGS: 417, - KHR: 116, - KMF: 174, - KPW: 408, - KRW: 410, - KWD: 414, - KYD: 136, - KZT: 398, - LAK: 418, - LBP: 422, - LKR: 144, - LRD: 430, - LSL: 426, - LYD: 434, - MAD: 504, - MDL: 498, - MGA: 969, - MKD: 807, - MMK: 104, - MNT: 496, - MOP: 446, - MRU: 929, - MUR: 480, - MVR: 462, - MWK: 454, - MXN: 484, - MXV: 979, - MYR: 458, - MZN: 943, - NAD: 516, - NGN: 566, - NIO: 558, - NOK: 578, - NPR: 524, - NZD: 554, - OMR: 512, - PAB: 590, - PEN: 604, - PGK: 598, - PHP: 608, - PKR: 586, - PLN: 985, - PYG: 600, - QAR: 634, - RON: 946, - RSD: 941, - RUB: 643, - RWF: 646, - SAR: 682, - SBD: 90, - SCR: 690, - SDG: 938, - SEK: 752, - SGD: 702, - SHP: 654, - SLE: 925, - SOS: 706, - SRD: 968, - SSP: 728, - STN: 930, - SVC: 222, - SYP: 760, - SZL: 748, - THB: 764, - TJS: 972, - TMT: 934, - TND: 788, - TOP: 776, - TRY: 949, - TTD: 780, - TWD: 901, - TZS: 834, - UAH: 980, - UGX: 800, - USD: 840, - USN: 997, - UYI: 940, - UYU: 858, - UYW: 927, - UZS: 860, - VED: 926, - VES: 928, - VND: 704, - VUV: 548, - WST: 882, - XAF: 950, - XAG: 961, - XAU: 959, - XBA: 955, - XBB: 956, - XBC: 957, - XBD: 958, - XCD: 951, - XDR: 960, - XOF: 952, - XPD: 964, - XPF: 953, - XPT: 962, - XSU: 994, - XTS: 963, - XUA: 965, - XXX: 999, - YER: 886, - ZAR: 710, - ZMW: 967, - ZWG: 924, - ZWL: 932, -} - -export const isIso4217CurrencyCode = (code: string): code is Iso4217AlphabeticalCode => Iso4217CurrencyCodes[code as Iso4217AlphabeticalCode] ? true : false diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts deleted file mode 100644 index bfd293c60..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Amount/Payload.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import type { Iso4217AlphabeticalCode } from './Iso4217Currency.ts' - -export const AmountSchema = 'network.xyo.payments.amount' as const -export type AmountSchema = typeof AmountSchema - -export interface AmountFields { - amount: number - currency: Iso4217AlphabeticalCode -} - -/** - * The result of a amount - */ -export type Amount = PayloadWithSources - -/** - * Identity function for determining if an object is an Amount - */ -export const isAmount = isPayloadOfSchemaType(AmountSchema) - -/** - * Identity function for determining if an object is an Amount with sources - */ -export const isAmountWithSources = isPayloadOfSchemaTypeWithSources(AmountSchema) - -/** - * Identity function for determining if an object is an Amount with meta - */ -export const isAmountWithMeta = isPayloadOfSchemaTypeWithMeta(AmountSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts b/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts deleted file mode 100644 index 155f9afe3..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Amount/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Iso4217Currency.ts' -export * from './Payload.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts deleted file mode 100644 index 842b2c64c..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Config.ts +++ /dev/null @@ -1,34 +0,0 @@ -// import type { Hash } from '@xylabs/hex' -import type { Address } from '@xylabs/hex' -import type { DivinerConfig } from '@xyo-network/diviner-model' -import type { ModuleIdentifier } from '@xyo-network/module-model' - -export const PaymentDiscountDivinerConfigSchema = 'network.xyo.diviner.payments.discount.config' -export type PaymentDiscountDivinerConfigSchema = typeof PaymentDiscountDivinerConfigSchema - -/** - * The configuration for the Payment Discount Diviner - */ -export type PaymentDiscountDivinerConfig = DivinerConfig< - { - /** - * The boundwitness diviner used to query for payloads - */ - boundWitnessDiviner?: ModuleIdentifier - /** - * The list of coupon authorities that can be used to get a discount - */ - couponAuthorities?: Address[] - - // /** - // * The list of coupons that are supported by this diviner - // */ - // supportedCoupons?: Hash[] - - /** - * The Diviner that can be used to determine the subtotal to apply discounts to - */ - paymentSubtotalDiviner?: ModuleIdentifier - }, - PaymentDiscountDivinerConfigSchema -> diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts deleted file mode 100644 index 8ffb0774e..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Params.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Address } from '@xylabs/hex' -import type { DivinerParams } from '@xyo-network/diviner-model' -import type { AnyConfigSchema } from '@xyo-network/module-model' - -import type { PaymentDiscountDivinerConfig } from './Config.ts' - -export type PaymentDiscountDivinerParams< - TConfig extends AnyConfigSchema = AnyConfigSchema, -> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts deleted file mode 100644 index e8ea3e731..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedAmount.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import type { AmountFields } from '../../../../Amount/index.ts' -import { CouponSchema } from '../Schema.ts' -import type { CouponFields } from '../types/index.ts' - -export const FixedAmountCouponSchema = `${CouponSchema}.fixed.amount` as const -export type FixedAmountCouponSchema = typeof FixedAmountCouponSchema - -export interface FixedAmountCouponFields extends CouponFields, AmountFields { - -} - -/** - * A coupon that provides a fixed discount amount - */ -export type FixedAmountCoupon = PayloadWithSources - -/** - * Identity function for determining if an object is an FixedAmountCoupon - */ -export const isFixedAmountCoupon = isPayloadOfSchemaType(FixedAmountCouponSchema) - -/** - * Identity function for determining if an object is an FixedAmountCoupon with sources - */ -export const isFixedAmountCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedAmountCouponSchema) - -/** - * Identity function for determining if an object is an FixedAmountCoupon with meta - */ -export const isFixedAmountCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedAmountCouponSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts deleted file mode 100644 index 1d6942e69..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/FixedPercentage.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import { CouponSchema } from '../Schema.ts' -import type { CouponFields } from '../types/index.ts' - -export const FixedPercentageCouponSchema = `${CouponSchema}.fixed.percentage` as const -export type FixedPercentageCouponSchema = typeof FixedPercentageCouponSchema - -export interface FixedPercentageCouponFields extends CouponFields { - percentage: number - -} - -/** - * A coupon that provides a fixed discount amount - */ -export type FixedPercentageCoupon = PayloadWithSources - -/** - * Identity function for determining if an object is an FixedPercentageCoupon - */ -export const isFixedPercentageCoupon = isPayloadOfSchemaType(FixedPercentageCouponSchema) - -/** - * Identity function for determining if an object is an FixedPercentageCoupon with sources - */ -export const isFixedPercentageCouponWithSources = isPayloadOfSchemaTypeWithSources(FixedPercentageCouponSchema) - -/** - * Identity function for determining if an object is an FixedPercentageCoupon with meta - */ -export const isFixedPercentageCouponWithMeta = isPayloadOfSchemaTypeWithMeta(FixedPercentageCouponSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts deleted file mode 100644 index ff31c45af..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Coupons/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './FixedAmount.ts' -export * from './FixedPercentage.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts deleted file mode 100644 index 40bae5a2a..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Payload.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - type FixedAmountCoupon, type FixedPercentageCoupon, isFixedAmountCoupon, isFixedAmountCouponWithMeta, isFixedAmountCouponWithSources, isFixedPercentageCoupon, - isFixedPercentageCouponWithMeta, - isFixedPercentageCouponWithSources, -} from './Coupons/index.ts' - -/** - * The result of a discount - */ -export type Coupon = FixedAmountCoupon | FixedPercentageCoupon - -/** - * Identity function for determining if an object is an Coupon - */ -export const isCoupon = (x?: unknown | null) => isFixedAmountCoupon(x) || isFixedPercentageCoupon(x) - -/** - * Identity function for determining if an object is an Coupon with sources - */ -export const isCouponWithSources = (x?: unknown | null) => isFixedAmountCouponWithSources(x) || isFixedPercentageCouponWithSources(x) - -/** - * Identity function for determining if an object is an Coupon with meta - */ -export const isCouponWithMeta = (x?: unknown | null) => isFixedAmountCouponWithMeta(x) || isFixedPercentageCouponWithMeta(x) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts deleted file mode 100644 index 4cf0542af..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/Schema.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CouponSchema = 'network.xyo.payments.coupon' as const -export type CouponSchema = typeof CouponSchema diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts deleted file mode 100644 index 02c47de6f..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Coupons/index.ts' -export * from './Payload.ts' -export * from './Schema.ts' -export * from './types/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts deleted file mode 100644 index 18e472798..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/CouponFields.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' - -/** - * The fields that are common across all coupons - */ -export interface CouponFields extends DurationFields { - /** - * Whether or not this discount can be stacked with other discounts - */ - stackable?: boolean -} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts deleted file mode 100644 index ee5290191..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './CouponFields.ts' -export * from './isStackable.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts deleted file mode 100644 index a0fb58e29..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Coupon/types/isStackable.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { CouponFields } from './CouponFields.ts' - -/** - * Identity function for determining if coupon is stackable - */ -export const isStackable = (x?: unknown | null) => (x as CouponFields ?? { stackable: false })?.stackable diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts deleted file mode 100644 index f5fab74f8..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/Discount.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import type { AmountFields } from '../../Amount/index.ts' - -export const DiscountSchema = 'network.xyo.payments.discount' as const -export type DiscountSchema = typeof DiscountSchema - -export interface DiscountFields extends AmountFields { } - -/** - * The result of a discount - */ -export type Discount = PayloadWithSources - -/** - * Identity function for determining if an object is an Discount - */ -export const isDiscount = isPayloadOfSchemaType(DiscountSchema) - -/** - * Identity function for determining if an object is an Discount with sources - */ -export const isDiscountWithSources = isPayloadOfSchemaTypeWithSources(DiscountSchema) - -/** - * Identity function for determining if an object is an Discount with meta - */ -export const isDiscountWithMeta = isPayloadOfSchemaTypeWithMeta(DiscountSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts deleted file mode 100644 index 8075f78a7..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/NoDiscount.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Discount } from './Discount.ts' -import { DiscountSchema } from './Discount.ts' - -export const NO_DISCOUNT: Discount = { - schema: DiscountSchema, - amount: 0, - currency: 'USD', -} diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts deleted file mode 100644 index a39b81c5c..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/Payload/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Coupon/index.ts' -export * from './Discount.ts' -export * from './NoDiscount.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts b/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts deleted file mode 100644 index 6ee3fc3b0..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Discount/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Config.ts' -export * from './Diviner.ts' -export * from './lib/index.ts' -export * from './Params.ts' -export * from './Payload/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts deleted file mode 100644 index 42b7e9050..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Invoice/Invoice.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Payment } from '@xyo-network/payment-payload-plugins' - -import type { Discount } from '../Discount/index.ts' -import type { Subtotal } from '../Subtotal/index.ts' -import type { Total } from '../Total/index.ts' - -/** - * A tuple containing the subtotal, total, and payment for an invoice. - */ -export type StandardInvoice = [Subtotal, Total, Payment] - -/** - * A tuple containing the subtotal, total, payment, and discount for an invoice. - */ -export type DiscountedInvoice = [...StandardInvoice, Discount] - -/** - * An invoice. - */ -export type Invoice = StandardInvoice | DiscountedInvoice diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts deleted file mode 100644 index 04d2059c4..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/getInvoiceForEscrow.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { assertEx } from '@xylabs/assert' -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' -import { MemoryNode } from '@xyo-network/node-memory' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' - -import { NO_DISCOUNT, PaymentDiscountDiviner } from '../../Discount/index.ts' -import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' -import type { PaymentTotalDivinerConfig } from '../../Total/index.ts' -import { PaymentTotalDiviner } from '../../Total/index.ts' -import { getInvoiceForEscrow } from '../getInvoiceForEscrow.ts' - -describe('getInvoiceForEscrow', () => { - let node: MemoryNode - let paymentDiscountDiviner: PaymentDiscountDiviner - let paymentSubtotalDiviner: PaymentSubtotalDiviner - let paymentTotalDiviner: PaymentTotalDiviner - beforeAll(async () => { - node = await MemoryNode.create({ account: 'random' }) - paymentDiscountDiviner = await PaymentDiscountDiviner.create({ account: 'random' }) - paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) - const config: PaymentTotalDivinerConfig = { - paymentDiscountDiviner: paymentDiscountDiviner.address, - paymentSubtotalDiviner: paymentSubtotalDiviner.address, - schema: PaymentTotalDiviner.defaultConfigSchema, - } - paymentTotalDiviner = await PaymentTotalDiviner.create({ account: 'random', config }) - const modules = [paymentDiscountDiviner, paymentSubtotalDiviner, paymentTotalDiviner] - for (const module of modules) { - await node.register(module) - await node.attach(module.address, true) - } - }) - describe('with no discount', () => { - it('should return invoice values', async () => { - const nbf = Date.now() - const exp = nbf + 1000 * 60 * 10 - const appraisal: HashLeaseEstimate = { - price: 10, currency: 'USD', schema: HashLeaseEstimateSchema, exp, nbf, - } - const appraisalHash = await PayloadBuilder.dataHash(appraisal) - const terms: EscrowTerms = { - schema: EscrowTermsSchema, appraisals: [appraisalHash], exp, nbf, - } - const dataHashMap = await PayloadBuilder.toDataHashMap([appraisal]) - const result = await getInvoiceForEscrow(terms, dataHashMap, paymentTotalDiviner) - expect(result).toBeArray() - expect(result?.length).toBeGreaterThan(0) - const invoice = assertEx(result) - const [subtotal, total, payment, discount] = invoice - expect(subtotal).toBeDefined() - expect(subtotal.amount).toBeNumber() - expect(subtotal.amount).toBe(appraisal.price) - expect(subtotal.currency).toBe('USD') - expect(total).toBeDefined() - expect(total.amount).toBeNumber() - expect(total.amount).toBe(subtotal.amount) - expect(total.currency).toBe('USD') - expect(payment).toBeDefined() - expect(payment.amount).toBeNumber() - expect(payment.amount).toBe(total.amount) - expect(payment.currency).toBe('USD') - expect(discount).toBeDefined() - expect(discount).toMatchObject(NO_DISCOUNT) - }) - }) -}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json deleted file mode 100644 index 16980bd0b..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Invoice/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@xylabs/tsconfig-jest" -} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts deleted file mode 100644 index 8eb263689..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Config.ts +++ /dev/null @@ -1,13 +0,0 @@ -// import type { Hash } from '@xylabs/hex' -import type { DivinerConfig } from '@xyo-network/diviner-model' - -export const PaymentSubtotalDivinerConfigSchema = 'network.xyo.diviner.payments.subtotal.config' -export type PaymentSubtotalDivinerConfigSchema = typeof PaymentSubtotalDivinerConfigSchema - -/** - * The configuration for the Coupon Subtotal Diviner - */ -export type PaymentSubtotalDivinerConfig = DivinerConfig< - {}, - PaymentSubtotalDivinerConfigSchema -> diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts deleted file mode 100644 index 8491c1d9e..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Diviner.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AbstractDiviner } from '@xyo-network/diviner-abstract' -import { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model' -import { creatableModule } from '@xyo-network/module-model' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import { Payload } from '@xyo-network/payload-model' -import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' - -import { PaymentSubtotalDivinerConfigSchema } from './Config.ts' -import { - appraisalValidators, termsValidators, ValidEscrowTerms, -} from './lib/index.ts' -import { PaymentSubtotalDivinerParams } from './Params.ts' -import { Subtotal, SubtotalSchema } from './Payload.ts' - -const currency = 'USD' - -/** - * Escrow terms that contain all the valid fields for calculating a subtotal - */ -export type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload - -@creatableModule() -export class PaymentSubtotalDiviner< - TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams, - TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType, - TOut extends Subtotal = Subtotal, - TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< - DivinerInstance, - TIn, - TOut - >, -> extends AbstractDiviner { - static override configSchemas = [PaymentSubtotalDivinerConfigSchema] - static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema - - protected async divineHandler(payloads: TIn[] = []): Promise { - // Find the escrow terms - const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined - if (!terms) return [] - - // Run all terms validations - if (!termsValidators.every(validator => validator(terms))) return [] - const validTerms = terms as ValidEscrowTerms - - // Retrieve all appraisals from terms - const hashMap = await PayloadBuilder.toAllHashMap(payloads) - const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] - - // Ensure all appraisals are present - if (appraisals.length !== validTerms.appraisals.length) return [] - - // Run all appraisal validations - if (!appraisalValidators.every(validator => validator(appraisals))) return [] - const amount = calculateSubtotal(appraisals) - const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals] - return [{ - amount, currency, schema: SubtotalSchema, sources, - }] as TOut[] - } -} - -// TODO: Add support for other currencies -const calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => { - return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0) -} diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts deleted file mode 100644 index 7e05fbc3a..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Params.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DivinerParams } from '@xyo-network/diviner-model' -import type { AnyConfigSchema } from '@xyo-network/module-model' - -import type { PaymentSubtotalDivinerConfig } from './Config.ts' - -export type PaymentSubtotalDivinerParams< - TConfig extends AnyConfigSchema = AnyConfigSchema, -> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts deleted file mode 100644 index cce562286..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/Payload.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PayloadWithSources } from '@xyo-network/payload-model' -import { - isPayloadOfSchemaType, - isPayloadOfSchemaTypeWithMeta, - isPayloadOfSchemaTypeWithSources, -} from '@xyo-network/payload-model' - -import type { AmountFields } from '../Amount/index.ts' - -export const SubtotalSchema = 'network.xyo.payments.subtotal' as const -export type SubtotalSchema = typeof SubtotalSchema - -export interface SubtotalFields extends AmountFields {} - -/** - * The result of a subtotal - */ -export type Subtotal = PayloadWithSources - -/** - * Identity function for determining if an object is an Subtotal - */ -export const isSubtotal = isPayloadOfSchemaType(SubtotalSchema) - -/** - * Identity function for determining if an object is an Subtotal with sources - */ -export const isSubtotalWithSources = isPayloadOfSchemaTypeWithSources(SubtotalSchema) - -/** - * Identity function for determining if an object is an Subtotal with meta - */ -export const isSubtotalWithMeta = isPayloadOfSchemaTypeWithMeta(SubtotalSchema) diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts deleted file mode 100644 index 94f009b80..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/appraisalValidators.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' - -import { isIso4217CurrencyCode } from '../../Amount/index.ts' -import { validateDuration } from './durationValidators.ts' - -const validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => { - // Ensure all appraisals are numeric - if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false - // Ensure all appraisals are positive numbers - if (appraisals.some(appraisal => appraisal.price < 0)) return false - return true -} - -const validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => { - // NOTE: Only supporting USD for now, the remaining checks are for future-proofing. - if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false - - // Check every object in the array to ensure they all are in a supported currency. - if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false - - return true -} - -const validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => { - // Check if the array is empty or contains only one element, no need to compare. - if (appraisals.length <= 1) return true - - // Get the currency of the first element to compare with others. - const { currency } = appraisals[0] - if (!currency) return false - - // Check every object in the array to ensure they all have the same currency. - if (!appraisals.every(item => item.currency === currency)) return false - - return true -} - -const validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration) - -export const appraisalValidators = [ - validateAppraisalAmount, - validateAppraisalCurrency, - validateAppraisalConsistentCurrency, - validateAppraisalWindow, -] diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts deleted file mode 100644 index 52831e574..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/durationValidators.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { DurationFields } from '@xyo-network/xns-record-payload-plugins' - -const FIVE_MINUTES = 1000 * 60 * 5 - -/** - * Validates that the current time is within the duration window, within a configurable a buffer - * @param value The duration value - * @param windowMs The window in milliseconds to allow for a buffer - * @returns True if the duration is valid, false otherwise - */ -export const validateDuration = (value: Partial, windowMs = FIVE_MINUTES): boolean => { - const now = Date.now() - if (!value.nbf || value.nbf > now) return false - // If already expired (include for a 5 minute buffer to allow for a reasonable - // minimum amount of time for the transaction to be processed) - if (!value.exp || value.exp - now < windowMs) return false - return true -} diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts deleted file mode 100644 index c454d02eb..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './appraisalValidators.ts' -export * from './termsValidators.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts deleted file mode 100644 index c447a824b..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/lib/termsValidators.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Hash } from '@xylabs/hex' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' - -import { validateDuration } from './durationValidators.ts' - -export type ValidEscrowTerms = Required - -const validateTermsAppraisals = (terms: EscrowTerms): terms is Required => { - if (!terms.appraisals) return false - if (terms.appraisals.length === 0) return false - return true -} -const validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms) - -export const termsValidators = [ - validateTermsAppraisals, - validateTermsWindow, -] diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts deleted file mode 100644 index b31c90e33..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/Diviner.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' -import { - beforeAll, beforeEach, describe, it, vi, -} from 'vitest' - -import { PaymentSubtotalDiviner } from '../Diviner.ts' -import { - isSubtotal, - SubtotalSchema, -} from '../Payload.ts' - -describe('PaymentSubtotalDiviner', () => { - let sut: PaymentSubtotalDiviner - const nbf = Date.now() - const exp = Number.MAX_SAFE_INTEGER - const termsBase: EscrowTerms = { - schema: EscrowTermsSchema, appraisals: [], exp, nbf, - } - const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ - [ - [ - { - schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, - }, - ], 11], - [ - [ - { - schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, - }, - ], 30], - [ - [ - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - ], 300], - ] - beforeEach(() => { - vi.clearAllMocks() - }) - beforeAll(async () => { - sut = await PaymentSubtotalDiviner.create({ account: 'random' }) - }) - describe('with escrow terms containing valid appraisals', () => { - it.each(cases)('calculates the subtotal of all the appraisals', async (appraisals, total) => { - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } - const results = await sut.divine([terms, ...appraisals]) - expect(results).toBeArrayOfSize(1) - const result = results.find(isSubtotal) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: total, schema: SubtotalSchema }) - expect(result?.sources).toEqual([await PayloadBuilder.dataHash(terms), ...appraisalHashes]) - }) - }) - describe('with escrow terms containing invalid appraisals', () => { - describe('when containing negative values in appraisals', () => { - it('calculates the subtotal of all the appraisals', async () => { - const appraisals = [{ - schema: HashLeaseEstimateSchema, price: -1, currency: 'USD', exp, nbf, - }] - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } - const results = await sut.divine([terms, ...appraisals]) - expect(results).toBeArrayOfSize(0) - }) - }) - describe('when containing non-numeric values in appraisals', () => { - it('calculates the subtotal of all the appraisals', async () => { - const appraisals = [{ - schema: HashLeaseEstimateSchema, price: 'three dollars', currency: 'USD', exp, nbf, - }] - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } - const results = await sut.divine([terms, ...appraisals]) - expect(results).toBeArrayOfSize(0) - }) - }) - describe('when containing mixed currencies in appraisals', () => { - it('calculates the subtotal of all the appraisals', async () => { - const appraisals = [{ - schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, - }, { - schema: HashLeaseEstimateSchema, price: 1, currency: 'EUR', exp, nbf, - }] - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } - const results = await sut.divine([terms, ...appraisals]) - expect(results).toBeArrayOfSize(0) - }) - }) - }) -}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json deleted file mode 100644 index 16980bd0b..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Subtotal/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@xylabs/tsconfig-jest" -} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts deleted file mode 100644 index 143770b1a..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Total/Config.ts +++ /dev/null @@ -1,24 +0,0 @@ -// import type { Hash } from '@xylabs/hex' -import type { DivinerConfig } from '@xyo-network/diviner-model' -import type { ModuleIdentifier } from '@xyo-network/module-model' - -export const PaymentTotalDivinerConfigSchema = 'network.xyo.diviner.payments.total.config' -export type PaymentTotalDivinerConfigSchema = typeof PaymentTotalDivinerConfigSchema - -/** - * The configuration for the Total Diviner - */ -export type PaymentTotalDivinerConfig = DivinerConfig< - { - /** - * The Diviner that will be used to determine the discount - */ - paymentDiscountDiviner?: ModuleIdentifier - - /** - * The Diviner that will be used to determine the subtotal - */ - paymentSubtotalDiviner?: ModuleIdentifier - }, - PaymentTotalDivinerConfigSchema -> diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts deleted file mode 100644 index 9db4086c6..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Total/Diviner.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { assertEx } from '@xylabs/assert' -import { Hash } from '@xylabs/hex' -import { AbstractDiviner } from '@xyo-network/diviner-abstract' -import { - asDivinerInstance, DivinerInstance, DivinerModuleEventData, -} from '@xyo-network/diviner-model' -import { creatableModule } from '@xyo-network/module-model' - -import { - Discount, - isDiscount, PaymentDiscountDiviner, - PaymentDiscountDivinerInputType, -} from '../Discount/index.ts' -import { - isSubtotal, PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType, Subtotal, -} from '../Subtotal/index.ts' -import { PaymentTotalDivinerConfigSchema } from './Config.ts' -import { PaymentTotalDivinerParams } from './Params.ts' -import { Total, TotalSchema } from './Payload.ts' - -type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType -type OutputType = Subtotal | Discount | Total - -@creatableModule() -export class PaymentTotalDiviner< - TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams, - TIn extends InputType = InputType, - TOut extends OutputType = OutputType, - TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< - DivinerInstance, - TIn, - TOut - >, -> extends AbstractDiviner { - static override configSchemas = [PaymentTotalDivinerConfigSchema] - static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema - - protected async divineHandler(payloads: TIn[] = []): Promise { - const subtotalDiviner = await this.getPaymentSubtotalDiviner() - const subtotalResult = await subtotalDiviner.divine(payloads) - const subtotal = subtotalResult.find(isSubtotal) - if (!subtotal) return [] - const discountDiviner = await this.getPaymentDiscountsDiviner() - const discountResult = await discountDiviner.divine(payloads) - const discount = discountResult.find(isDiscount) - if (!discount) return [] - const { currency: subtotalCurrency } = subtotal - const { currency: discountCurrency } = discount - assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`) - const amount = Math.max(0, subtotal.amount - discount.amount) - const currency = subtotalCurrency - const sources = [subtotal.$hash, discount.$hash] as Hash[] - const total: Total = { - amount, currency, sources, schema: TotalSchema, - } - return [subtotal, discount, total] as TOut[] - } - - protected async getPaymentDiscountsDiviner(): Promise { - const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config') - const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`) - return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner - } - - protected async getPaymentSubtotalDiviner(): Promise { - const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config') - const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`) - return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner - } -} diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts b/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts deleted file mode 100644 index 07942f3ba..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Total/Params.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { DivinerParams } from '@xyo-network/diviner-model' -import type { AnyConfigSchema } from '@xyo-network/module-model' - -import type { PaymentTotalDivinerConfig } from './Config.ts' - -export type PaymentTotalDivinerParams< - TConfig extends AnyConfigSchema = AnyConfigSchema, -> = DivinerParams diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts deleted file mode 100644 index dbbce2ac6..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Total/spec/Diviner.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { HDWallet } from '@xyo-network/account' -import { MemoryArchivist } from '@xyo-network/archivist-memory' -import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' -import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' -import { MemoryNode } from '@xyo-network/node-memory' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' -import { - beforeAll, beforeEach, describe, it, vi, -} from 'vitest' - -import type { Coupon } from '../../Discount/index.ts' -import { - FixedAmountCouponSchema, FixedPercentageCouponSchema, PaymentDiscountDiviner, -} from '../../Discount/index.ts' -import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' -import { PaymentTotalDiviner } from '../Diviner.ts' -import { - isTotal, - TotalSchema, -} from '../Payload.ts' - -describe('PaymentTotalDiviner', () => { - let sut: PaymentTotalDiviner - const nbf = Date.now() - const exp = nbf + 1000 * 60 * 10 - const termsBase: EscrowTerms = { - schema: EscrowTermsSchema, appraisals: [], exp, nbf, - } - const cases: [estimates: HashLeaseEstimate[], subtotal: number][] = [ - [ - [ - { - schema: HashLeaseEstimateSchema, price: 1, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, - }, - ], 11], - [ - [ - { - schema: HashLeaseEstimateSchema, price: 10, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 20, currency: 'USD', exp, nbf, - }, - ], 30], - [ - [ - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - { - schema: HashLeaseEstimateSchema, price: 100, currency: 'USD', exp, nbf, - }, - ], 300], - ] - const validCoupons: Coupon[] = [ - { - amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', - }, - { - percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, - }, - ] - const unsignedCoupons: Coupon[] = [ - { - amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', - }, - { - percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, - }, - ] - beforeEach(() => { - vi.clearAllMocks() - }) - beforeAll(async () => { - const node = await MemoryNode.create({ account: 'random' }) - expect(node).toBeDefined() - const paymentSubtotalDiviner = await PaymentSubtotalDiviner.create({ account: 'random' }) - const discountsArchivist = await MemoryArchivist.create({ account: 'random' }) - const signer = await HDWallet.random() - // Sign the valid coupons and insert them into the archivist - for (const coupon of validCoupons) { - const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() - await discountsArchivist.insert([bw, ...payloads]) - } - // Insert (but do not sign) the unsigned coupons into the archivist - await discountsArchivist.insert(unsignedCoupons) - const discountsBoundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ - account: 'random', - config: { - archivist: discountsArchivist.address, - schema: MemoryBoundWitnessDiviner.defaultConfigSchema, - }, - }) - const discountDiviner = await PaymentDiscountDiviner.create({ - account: 'random', - config: { - archivist: discountsArchivist.address, - boundWitnessDiviner: discountsBoundWitnessDiviner.address, - couponAuthorities: [signer.address], - schema: PaymentDiscountDiviner.defaultConfigSchema, - }, - }) - sut = await PaymentTotalDiviner.create({ - account: 'random', - config: { - paymentDiscountDiviner: discountDiviner.address, - paymentSubtotalDiviner: paymentSubtotalDiviner.address, - schema: PaymentTotalDiviner.defaultConfigSchema, - }, - }) - - const modules = [paymentSubtotalDiviner, discountsArchivist, discountsBoundWitnessDiviner, discountDiviner, sut] - for (const mod of modules) { - await node.register(mod) - await node.attach(mod.address, true) - } - }) - describe('with valid escrow', () => { - describe('with no discounts', () => { - it.each(cases)('calculates total', async (appraisals, total) => { - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { ...termsBase, appraisals: appraisalHashes } - const results = await sut.divine([terms, ...appraisals]) - expect(results).toBeArrayOfSize(3) - const result = results.find(isTotal) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: total, schema: TotalSchema }) - }) - }) - describe('with valid discounts', () => { - describe.each(cases)('calculates total', (appraisals, total) => { - it.each(validCoupons)('applying coupon discount to total', async (coupon) => { - const discounts = await PayloadBuilder.dataHashes([coupon]) - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { - ...termsBase, appraisals: appraisalHashes, discounts, - } - const results = await sut.divine([terms, ...appraisals, coupon]) - expect(results).toBeArrayOfSize(3) - const result = results.find(isTotal) - expect(result).toBeDefined() - expect(result?.amount).toBeLessThan(total) - }) - }) - }) - describe('with invalid discounts', () => { - describe.each(cases)('calculates total', (appraisals, total) => { - it.each(unsignedCoupons)('without applying coupon discount to total', async (coupon) => { - const discounts = await PayloadBuilder.dataHashes([coupon]) - const appraisalHashes = await PayloadBuilder.dataHashes(appraisals) - const terms: EscrowTerms = { - ...termsBase, appraisals: appraisalHashes, discounts, - } - const results = await sut.divine([terms, ...appraisals, coupon]) - expect(results).toBeArrayOfSize(3) - const result = results.find(isTotal) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: total, schema: TotalSchema }) - }) - }) - }) - }) -}) diff --git a/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json deleted file mode 100644 index 16980bd0b..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/Total/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@xylabs/tsconfig-jest" -} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/src/Checkout/index.ts b/packages/payloadset/packages/payments/src/Checkout/index.ts deleted file mode 100644 index 16d47c683..000000000 --- a/packages/payloadset/packages/payments/src/Checkout/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Amount/index.ts' -export * from './Discount/index.ts' -export * from './Invoice/index.ts' -export * from './Subtotal/index.ts' -export * from './Total/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/Diviner.ts rename to packages/payloadset/packages/payments/src/Discount/Diviner.ts diff --git a/packages/payloadset/packages/payments/src/Discount/index.ts b/packages/payloadset/packages/payments/src/Discount/index.ts new file mode 100644 index 000000000..3188edcd1 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Discount/index.ts @@ -0,0 +1,2 @@ +export * from './Diviner.ts' +export * from './lib/index.ts' diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts b/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/lib/applyCoupons.ts rename to packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts b/packages/payloadset/packages/payments/src/Discount/lib/index.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/lib/index.ts rename to packages/payloadset/packages/payments/src/Discount/lib/index.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts b/packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/applyCoupons.spec.ts rename to packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts diff --git a/packages/payload/packages/payments/src/Invoice/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Discount/lib/spec/tsconfig.json similarity index 100% rename from packages/payload/packages/payments/src/Invoice/spec/tsconfig.json rename to packages/payloadset/packages/payments/src/Discount/lib/spec/tsconfig.json diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/spec/Diviner.spec.ts rename to packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts diff --git a/packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Discount/spec/tsconfig.json similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/spec/tsconfig.json rename to packages/payloadset/packages/payments/src/Discount/spec/tsconfig.json diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts b/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Invoice/getInvoiceForEscrow.ts rename to packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts b/packages/payloadset/packages/payments/src/Invoice/index.ts similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Invoice/index.ts rename to packages/payloadset/packages/payments/src/Invoice/index.ts diff --git a/packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts similarity index 100% rename from packages/payload/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts rename to packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts diff --git a/packages/payload/packages/payments/src/Total/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Invoice/spec/tsconfig.json similarity index 100% rename from packages/payload/packages/payments/src/Total/spec/tsconfig.json rename to packages/payloadset/packages/payments/src/Invoice/spec/tsconfig.json diff --git a/packages/payload/packages/payments/src/Subtotal/Diviner.ts b/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/Diviner.ts rename to packages/payloadset/packages/payments/src/Subtotal/Diviner.ts diff --git a/packages/payloadset/packages/payments/src/Subtotal/index.ts b/packages/payloadset/packages/payments/src/Subtotal/index.ts new file mode 100644 index 000000000..db323902e --- /dev/null +++ b/packages/payloadset/packages/payments/src/Subtotal/index.ts @@ -0,0 +1 @@ +export * from './Diviner.ts' diff --git a/packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/lib/appraisalValidators.ts rename to packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts diff --git a/packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/durationValidators.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/lib/durationValidators.ts rename to packages/payloadset/packages/payments/src/Subtotal/lib/durationValidators.ts diff --git a/packages/payload/packages/payments/src/Subtotal/lib/index.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/index.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/lib/index.ts rename to packages/payloadset/packages/payments/src/Subtotal/lib/index.ts diff --git a/packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/termsValidators.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/lib/termsValidators.ts rename to packages/payloadset/packages/payments/src/Subtotal/lib/termsValidators.ts diff --git a/packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts similarity index 100% rename from packages/payload/packages/payments/src/Subtotal/spec/Diviner.spec.ts rename to packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Subtotal/spec/tsconfig.json similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/lib/spec/tsconfig.json rename to packages/payloadset/packages/payments/src/Subtotal/spec/tsconfig.json diff --git a/packages/payload/packages/payments/src/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Total/Diviner.ts similarity index 100% rename from packages/payload/packages/payments/src/Total/Diviner.ts rename to packages/payloadset/packages/payments/src/Total/Diviner.ts diff --git a/packages/payloadset/packages/payments/src/Total/index.ts b/packages/payloadset/packages/payments/src/Total/index.ts new file mode 100644 index 000000000..db323902e --- /dev/null +++ b/packages/payloadset/packages/payments/src/Total/index.ts @@ -0,0 +1 @@ +export * from './Diviner.ts' diff --git a/packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts similarity index 100% rename from packages/payload/packages/payments/src/Total/spec/Diviner.spec.ts rename to packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts diff --git a/packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Total/spec/tsconfig.json similarity index 100% rename from packages/payloadset/packages/payments/src/Checkout/Discount/spec/tsconfig.json rename to packages/payloadset/packages/payments/src/Total/spec/tsconfig.json diff --git a/packages/payloadset/packages/payments/src/index.ts b/packages/payloadset/packages/payments/src/index.ts index e69de29bb..d8691959e 100644 --- a/packages/payloadset/packages/payments/src/index.ts +++ b/packages/payloadset/packages/payments/src/index.ts @@ -0,0 +1,4 @@ +export * from './Discount/index.ts' +export * from './Invoice/index.ts' +export * from './Subtotal/index.ts' +export * from './Total/index.ts' diff --git a/yarn.lock b/yarn.lock index 09b28a859..ab4d3e67a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7078,6 +7078,7 @@ __metadata: "@xyo-network/boundwitness-model": "npm:^3.1.9" "@xyo-network/boundwitness-validator": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" + "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/id-payload-plugin": "npm:^3.1.9" "@xyo-network/module-model": "npm:^3.1.9" "@xyo-network/payload-model": "npm:^3.1.9" @@ -7085,6 +7086,29 @@ __metadata: languageName: unknown linkType: soft +"@xyo-network/payment-plugin@workspace:packages/payloadset/packages/payments": + version: 0.0.0-use.local + resolution: "@xyo-network/payment-plugin@workspace:packages/payloadset/packages/payments" + dependencies: + "@xylabs/assert": "npm:^4.0.9" + "@xylabs/axios": "npm:^4.0.9" + "@xylabs/jest-helpers": "npm:^4.0.9" + "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" + "@xylabs/tsconfig": "npm:^4.0.7" + "@xyo-network/boundwitness-model": "npm:^3.1.9" + "@xyo-network/module-model": "npm:^3.1.9" + "@xyo-network/payload-builder": "npm:^3.1.9" + "@xyo-network/payload-model": "npm:^3.1.9" + "@xyo-network/payment-payload-plugins": "workspace:^" + "@xyo-network/rebilly-payment-payload-plugin": "workspace:^" + "@xyo-network/sentinel-abstract": "npm:^3.1.9" + "@xyo-network/sentinel-model": "npm:^3.1.9" + axios: "npm:^1.7.7" + jest: "npm:^29.7.0" + typescript: "npm:^5.5.4" + languageName: unknown + linkType: soft + "@xyo-network/pentair-payload-plugin@workspace:^, @xyo-network/pentair-payload-plugin@workspace:packages/payload/packages/pentair": version: 0.0.0-use.local resolution: "@xyo-network/pentair-payload-plugin@workspace:packages/payload/packages/pentair" From 010e342626cd2e9b7698d3c257a08b75884544ce Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 19:47:14 -0500 Subject: [PATCH 3/9] Update exports --- packages/payload/packages/payments/src/Invoice/index.ts | 1 - packages/payload/packages/payments/src/Total/Diviner/index.ts | 1 - packages/payloadset/packages/payments/src/Invoice/index.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/payload/packages/payments/src/Invoice/index.ts b/packages/payload/packages/payments/src/Invoice/index.ts index 2878663f2..faed65398 100644 --- a/packages/payload/packages/payments/src/Invoice/index.ts +++ b/packages/payload/packages/payments/src/Invoice/index.ts @@ -1,2 +1 @@ -export * from './getInvoiceForEscrow.ts' export * from './Invoice.ts' diff --git a/packages/payload/packages/payments/src/Total/Diviner/index.ts b/packages/payload/packages/payments/src/Total/Diviner/index.ts index 7a0ddb968..ccda19e2f 100644 --- a/packages/payload/packages/payments/src/Total/Diviner/index.ts +++ b/packages/payload/packages/payments/src/Total/Diviner/index.ts @@ -1,4 +1,3 @@ export * from './Config.ts' -export * from './Diviner.ts' export * from './Params.ts' export * from './Payload.ts' diff --git a/packages/payloadset/packages/payments/src/Invoice/index.ts b/packages/payloadset/packages/payments/src/Invoice/index.ts index 2878663f2..cc3b200e8 100644 --- a/packages/payloadset/packages/payments/src/Invoice/index.ts +++ b/packages/payloadset/packages/payments/src/Invoice/index.ts @@ -1,2 +1 @@ export * from './getInvoiceForEscrow.ts' -export * from './Invoice.ts' From e9e51988ff1bc92986f2076694932ad28bc2ca15 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 19:58:01 -0500 Subject: [PATCH 4/9] Remove diviner from payload plugins --- .../payload/packages/payments/package.json | 9 +- .../packages/payments/src/Discount/Diviner.ts | 158 ------------------ .../packages/payments/src/Discount/index.ts | 2 - .../payments/src/Discount/lib/applyCoupons.ts | 59 ------- .../payments/src/Discount/lib/index.ts | 1 - .../Discount/lib/spec/applyCoupons.spec.ts | 86 ---------- .../src/Discount/lib/spec/tsconfig.json | 3 - .../src/Discount/spec/Diviner.spec.ts | 127 -------------- .../payments/src/Discount/spec/tsconfig.json | 3 - .../payloadset/packages/payments/package.json | 8 + .../src/Invoice/getInvoiceForEscrow.ts | 16 +- yarn.lock | 31 +++- 12 files changed, 52 insertions(+), 451 deletions(-) delete mode 100644 packages/payload/packages/payments/src/Discount/Diviner.ts delete mode 100644 packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts delete mode 100644 packages/payload/packages/payments/src/Discount/lib/index.ts delete mode 100644 packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts delete mode 100644 packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json delete mode 100644 packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts delete mode 100644 packages/payload/packages/payments/src/Discount/spec/tsconfig.json diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index b71964720..f3db8e15d 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -31,16 +31,23 @@ "dependencies": { "@xylabs/assert": "^4.0.9", "@xylabs/crypto": "^4.0.9", + "@xylabs/exists": "^4.0.10", "@xylabs/hex": "^4.0.9", "@xyo-network/account-model": "^3.1.9", + "@xyo-network/archivist-model": "^3.1.9", "@xyo-network/boundwitness-builder": "^3.1.9", "@xyo-network/boundwitness-model": "^3.1.9", "@xyo-network/boundwitness-validator": "^3.1.9", + "@xyo-network/diviner-abstract": "^3.1.9", + "@xyo-network/diviner-boundwitness-model": "^3.1.9", "@xyo-network/diviner-hash-lease": "^3.1.9", "@xyo-network/diviner-model": "^3.1.9", "@xyo-network/id-payload-plugin": "^3.1.9", "@xyo-network/module-model": "^3.1.9", - "@xyo-network/payload-model": "^3.1.9" + "@xyo-network/payload-builder": "^3.1.9", + "@xyo-network/payload-model": "^3.1.9", + "@xyo-network/payment-payload-plugins": "^3.0.17", + "@xyo-network/xns-record-payload-plugins": "workspace:^" }, "devDependencies": { "@xylabs/ts-scripts-yarn3": "^4.0.7", diff --git a/packages/payload/packages/payments/src/Discount/Diviner.ts b/packages/payload/packages/payments/src/Discount/Diviner.ts deleted file mode 100644 index aeaedc751..000000000 --- a/packages/payload/packages/payments/src/Discount/Diviner.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { assertEx } from '@xylabs/assert' -import { exists } from '@xylabs/exists' -import { Address, Hash } from '@xylabs/hex' -import { ArchivistInstance, asArchivistInstance } from '@xyo-network/archivist-model' -import { AbstractDiviner } from '@xyo-network/diviner-abstract' -import { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model' -import { - HashLeaseEstimate, - isHashLeaseEstimate, -} from '@xyo-network/diviner-hash-lease' -import { - asDivinerInstance, DivinerInstance, DivinerModuleEventData, -} from '@xyo-network/diviner-model' -import { creatableModule } from '@xyo-network/module-model' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import { Payload } from '@xyo-network/payload-model' -import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' - -import { PaymentDiscountDivinerConfigSchema } from './Config.ts' -import { applyCoupons } from './lib/index.ts' -import { PaymentDiscountDivinerParams } from './Params.ts' -import { - Coupon, - Discount, - isCoupon, - isCouponWithMeta, - NO_DISCOUNT, -} from './Payload/index.ts' - -const DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly = { - limit: 1, - order: 'desc', - schema: BoundWitnessDivinerQuerySchema, -} - -export type PaymentDiscountDivinerInputType = EscrowTerms | Coupon | HashLeaseEstimate | Payload - -@creatableModule() -export class PaymentDiscountDiviner< - TParams extends PaymentDiscountDivinerParams = PaymentDiscountDivinerParams, - TIn extends PaymentDiscountDivinerInputType = PaymentDiscountDivinerInputType, - TOut extends Discount = Discount, - TEventData extends DivinerModuleEventData, TIn, TOut> = DivinerModuleEventData< - DivinerInstance, - TIn, - TOut - >, -> extends AbstractDiviner { - static override configSchemas = [PaymentDiscountDivinerConfigSchema] - static override defaultConfigSchema = PaymentDiscountDivinerConfigSchema - - protected get couponAuthorities(): Address[] { - return [...(this.config.couponAuthorities ?? []), ...(this.params.couponAuthorities ?? [])] - } - - protected async divineHandler(payloads: TIn[] = []): Promise { - const sources: Hash[] = [] - - // Parse terms - const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined - if (!terms) return [{ ...NO_DISCOUNT, sources }] as TOut[] - sources.push(await PayloadBuilder.hash(terms)) - - // Parse discounts - const discountHashes = terms.discounts ?? [] - if (discountHashes.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] - - // TODO: Call paymentSubtotalDiviner to get the subtotal to centralize the logic - // Parse appraisals - const termsAppraisals = terms?.appraisals - if (!termsAppraisals || termsAppraisals.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] - const hashMap = await PayloadBuilder.toAllHashMap(payloads) - const foundAppraisals = termsAppraisals.filter(hash => hashMap[hash]) - // Add the appraisals that were found to the sources - sources.push(...foundAppraisals) - // If not all appraisals are found, return no discount - if (foundAppraisals.length !== termsAppraisals.length) { - return [{ ...NO_DISCOUNT, sources }] as TOut[] - } - // TODO: Cast should not be required - const appraisals = foundAppraisals.map(hash => hashMap[hash]).filter(exists).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[] - - // Use the supplied payloads to find the discounts - const discounts = discountHashes.map(hash => hashMap[hash]).filter(exists).filter(isCoupon) as Coupon[] - // Find any remaining coupons from the archivist - if (discounts.length !== discountHashes.length) { - // Find remaining from discounts archivist - const discountsArchivist = await this.getDiscountsArchivist() - const foundDiscounts = await discountsArchivist.get(discountHashes) - discounts.push(...foundDiscounts.filter(isCouponWithMeta)) - } - const discountsMap = await PayloadBuilder.toAllHashMap(discounts) - if (Object.keys(discountsMap).length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] - - // Add the found discounts to the sources - const foundDiscountsHashes = Object.keys(discountsMap) as Hash[] - sources.push(...foundDiscountsHashes) - - // Log individual discounts that were not found - for (const hash of discountHashes) { - if (!foundDiscountsHashes.includes(hash)) { - console.warn(`Discount ${hash} not found for terms ${await PayloadBuilder.hash(terms)}`) - } - } - - // Parse coupons - const coupons = Object.values(discountsMap) - const validCoupons = await this.filterToSigned(coupons.filter(this.isCouponCurrent)) - if (validCoupons.length === 0) return [{ ...NO_DISCOUNT, sources }] as TOut[] - - const discount = applyCoupons(appraisals, validCoupons) - return [{ ...discount, sources }] as TOut[] - } - - /** - * Filters the supplied list of coupons to only those that are signed by - * addresses specified in the couponAuthorities - * @param coupons The list of coupons to filter - * @returns The filtered list of coupons that are signed by the couponAuthorities - */ - protected async filterToSigned(coupons: Coupon[]): Promise { - const signed: Coupon[] = [] - const dataHashMap = await PayloadBuilder.toDataHashMap(coupons) - const boundWitnessDiviner = await this.getDiscountsBoundWitnessDiviner() - const hashes = Object.keys(dataHashMap) - const addresses = this.couponAuthorities - // TODO: Keep an in memory cache of the hashes queried and their results - // to avoid querying the same hash multiple times - await Promise.all(hashes.map((h) => { - const hash = h as Hash - return Promise.all(addresses.map(async (address) => { - const query: BoundWitnessDivinerQueryPayload = { - ...DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS, addresses: [address], payload_hashes: [hash], - } - const result = await boundWitnessDiviner.divine([query]) - if (result.length > 0) signed.push(dataHashMap[hash]) - })) - })) - return signed - } - - protected async getDiscountsArchivist(): Promise { - const name = assertEx(this.config.archivist, () => 'Missing archivist in config') - const mod = assertEx(await this.resolve(name), () => `Error resolving archivist: ${name}`) - return assertEx(asArchivistInstance(mod), () => `Resolved module ${mod.address} not a valid Archivist`) - } - - protected async getDiscountsBoundWitnessDiviner(): Promise { - const name = assertEx(this.config.boundWitnessDiviner, () => 'Missing boundWitnessDiviner in config') - const mod = assertEx(await this.resolve(name), () => `Error resolving boundWitnessDiviner: ${name}`) - return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) - } - - protected isCouponCurrent(coupon: Coupon): boolean { - const now = Date.now() - return coupon.exp > now && coupon.nbf < now - } -} diff --git a/packages/payload/packages/payments/src/Discount/index.ts b/packages/payload/packages/payments/src/Discount/index.ts index 6ee3fc3b0..905e8a429 100644 --- a/packages/payload/packages/payments/src/Discount/index.ts +++ b/packages/payload/packages/payments/src/Discount/index.ts @@ -1,5 +1,3 @@ export * from './Config.ts' -export * from './Diviner.ts' -export * from './lib/index.ts' export * from './Params.ts' export * from './Payload/index.ts' diff --git a/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts b/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts deleted file mode 100644 index a079c7ea5..000000000 --- a/packages/payload/packages/payments/src/Discount/lib/applyCoupons.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { assertEx } from '@xylabs/assert' -import { exists } from '@xylabs/exists' -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' - -import type { AmountFields } from '../../Amount/index.ts' -import type { - Coupon, Discount, FixedAmountCoupon, - FixedPercentageCoupon, -} from '../Payload/index.ts' -import { - DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon, - isStackable, -} from '../Payload/index.ts' - -export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => { - // Ensure all appraisals and coupons are in USD - const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD') - assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD') - const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial)?.currency).filter(exists).every(currency => currency === 'USD') - assertEx(allCouponsAreUSD, 'All coupons must be in USD') - const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0) - - // Calculated non-stackable discount coupons - const singularFixedDiscount = Math.max(...coupons - .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon)) - .map(coupon => (coupon as FixedAmountCoupon).amount), 0) - const singularPercentageDiscount = (Math.max(...coupons - .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon)) - .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total - - // Calculate stackable discount coupons - // First calculate the total discount from fixed amount coupons - const stackedFixedDiscount = coupons - .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon)) - .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0) - // Then calculate the total discount from percentage coupons and apply - // the percentage discount to the remaining total after fixed discounts - const stackedPercentageDiscount = coupons - .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon)) - .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount) - // Sum all stackable discounts - const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount - - // Find the best coupon(s) to apply - const maxDiscount = Math.max( - singularFixedDiscount, - singularPercentageDiscount, - stackedDiscount, - 0, - ) - - // Ensure discount is not more than the total - const amount = Math.min(maxDiscount, total) - - // Return single discount payload - return { - amount, schema: DiscountSchema, currency: 'USD', - } -} diff --git a/packages/payload/packages/payments/src/Discount/lib/index.ts b/packages/payload/packages/payments/src/Discount/lib/index.ts deleted file mode 100644 index 89c610e91..000000000 --- a/packages/payload/packages/payments/src/Discount/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './applyCoupons.ts' diff --git a/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts b/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts deleted file mode 100644 index 5a40c6a14..000000000 --- a/packages/payload/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' -import { - beforeEach, describe, it, vi, -} from 'vitest' - -import type { Coupon } from '../../Payload/index.ts' -import { - DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, -} from '../../Payload/index.ts' -import { applyCoupons } from '../applyCoupons.ts' - -describe('applyCoupons', () => { - const nbf = Date.now() - const exp = Date.now() + 10_000_000 - // Coupons - const TEN_DOLLAR_OFF_COUPON: Coupon = { - amount: 10, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', - } - const TEN_PERCENT_OFF_COUPON: Coupon = { - percentage: 0.1, exp, nbf, schema: FixedPercentageCouponSchema, - } - // Appraisals - const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { - price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, - } - const SEVENTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { - price: 70, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, - } - const THIRTY_DOLLAR_ESTIMATE: HashLeaseEstimate = { - price: 30, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, - } - - beforeEach(() => { - vi.clearAllMocks() - }) - describe('when coupon is less than total', () => { - const validCoupons: [HashLeaseEstimate[], Coupon[]][] = [ - [[HUNDRED_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], - [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_DOLLAR_OFF_COUPON]], - [[HUNDRED_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], - [[SEVENTY_DOLLAR_ESTIMATE, THIRTY_DOLLAR_ESTIMATE], [TEN_PERCENT_OFF_COUPON]], - ] - it.each(validCoupons)('Applies coupon discount', (estimates, coupons) => { - const results = applyCoupons(estimates, coupons) - expect(results).toEqual({ - amount: 10, schema: DiscountSchema, currency: 'USD', - }) - }) - }) - describe('when discount exceeds total', () => { - const discountExceedsTotal: [HashLeaseEstimate[], Coupon[]][] = [ - [ - [HUNDRED_DOLLAR_ESTIMATE], - [{ - amount: 101, exp, nbf, schema: FixedAmountCouponSchema, currency: 'USD', - }]], - [ - [HUNDRED_DOLLAR_ESTIMATE], - [{ - percentage: 1.1, exp, nbf, schema: FixedPercentageCouponSchema, - }]], - ] - it.each(discountExceedsTotal)('Discounts only to total', (estimates, coupons) => { - const results = applyCoupons(estimates, coupons) - const amount = estimates.reduce((acc, a) => acc + a.price, 0) - expect(results).toEqual({ - amount, schema: DiscountSchema, currency: 'USD', - }) - }) - }) - describe('with stackable discounts', () => { - const STACKABLE_TEN_DOLLAR_OFF_COUPON: Coupon = { ...TEN_DOLLAR_OFF_COUPON, stackable: true } - const STACKABLE_TEN_PERCENT_OFF_COUPON: Coupon = { ...TEN_PERCENT_OFF_COUPON, stackable: true } - const validCoupons: [HashLeaseEstimate[], Coupon[], number][] = [ - [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_DOLLAR_OFF_COUPON, STACKABLE_TEN_PERCENT_OFF_COUPON], 19], - [[HUNDRED_DOLLAR_ESTIMATE], [STACKABLE_TEN_PERCENT_OFF_COUPON, STACKABLE_TEN_DOLLAR_OFF_COUPON], 19], - ] - it.each(validCoupons)('Applies both discounts', (estimates, coupons, amount) => { - const results = applyCoupons(estimates, coupons) - expect(results).toEqual({ - amount, schema: DiscountSchema, currency: 'USD', - }) - }) - }) -}) diff --git a/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json b/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json deleted file mode 100644 index 16980bd0b..000000000 --- a/packages/payload/packages/payments/src/Discount/lib/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@xylabs/tsconfig-jest" -} \ No newline at end of file diff --git a/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts b/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts deleted file mode 100644 index c7e32b7d3..000000000 --- a/packages/payload/packages/payments/src/Discount/spec/Diviner.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { HDWallet } from '@xyo-network/account' -import { MemoryArchivist } from '@xyo-network/archivist-memory' -import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder' -import { MemoryBoundWitnessDiviner } from '@xyo-network/diviner-boundwitness-memory' -import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' -import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' -import { MemoryNode } from '@xyo-network/node-memory' -import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' -import { - beforeEach, describe, it, vi, -} from 'vitest' - -import { PaymentDiscountDivinerConfigSchema } from '../Config.ts' -import { PaymentDiscountDiviner } from '../Diviner.ts' -import type { Coupon } from '../Payload/index.ts' -import { - DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, - isDiscount, -} from '../Payload/index.ts' - -describe('PaymentDiscountDiviner', () => { - let sut: PaymentDiscountDiviner - const nbf = Date.now() - const exp = Number.MAX_SAFE_INTEGER - const termsBase: EscrowTerms = { - schema: EscrowTermsSchema, appraisals: [], exp, nbf, - } - const HUNDRED_DOLLAR_ESTIMATE: HashLeaseEstimate = { - price: 100, currency: 'USD', exp, nbf, schema: HashLeaseEstimateSchema, - } - const validCoupons: Coupon[] = [ - { - amount: 10, exp, nbf: Date.now(), schema: FixedAmountCouponSchema, currency: 'USD', - }, - { - percentage: 0.1, exp, nbf: Date.now(), schema: FixedPercentageCouponSchema, - }, - ] - const unsignedCoupons: Coupon[] = [ - { - amount: 10, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', - }, - { - percentage: 0.1, exp: Number.MAX_SAFE_INTEGER, nbf: 0, schema: FixedPercentageCouponSchema, - }, - ] - beforeAll(async () => { - termsBase.appraisals?.push(await PayloadBuilder.hash(HUNDRED_DOLLAR_ESTIMATE)) - const node = await MemoryNode.create({ account: 'random' }) - const archivist = await MemoryArchivist.create({ account: 'random' }) - const signer = await HDWallet.random() - // Sign the valid coupons and insert them into the archivist - for (const coupon of validCoupons) { - const [bw, payloads] = await new BoundWitnessBuilder().signer(signer).payload(coupon).build() - await archivist.insert([bw, ...payloads]) - } - // Insert (but do not sign) the unsigned coupons into the archivist - await archivist.insert(unsignedCoupons) - const boundWitnessDiviner = await MemoryBoundWitnessDiviner.create({ - account: 'random', - config: { - archivist: archivist.address, - schema: MemoryBoundWitnessDiviner.defaultConfigSchema, - }, - }) - sut = await PaymentDiscountDiviner.create({ - account: 'random', - config: { - archivist: archivist.address, - boundWitnessDiviner: boundWitnessDiviner.address, - couponAuthorities: [signer.address], - schema: PaymentDiscountDivinerConfigSchema, - }, - }) - const modules = [archivist, boundWitnessDiviner, sut] - for (const mod of modules) { - await node.register(mod) - await node.attach(mod.address, false) - } - }) - beforeEach(() => { - vi.clearAllMocks() - }) - describe('with valid coupon', () => { - it.each(validCoupons)('Applies coupon', async (coupon) => { - const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } - const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) - expect(results).toBeArrayOfSize(1) - const result = results.find(isDiscount) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: 10, schema: DiscountSchema }) - }) - }) - describe('with invalid coupon', () => { - it.each(unsignedCoupons)('Does not apply coupons', async (coupon) => { - const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } - const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) - expect(results).toBeArrayOfSize(1) - const result = results.find(isDiscount) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) - }) - }) - describe('with expired coupon', () => { - const now = Date.now() - const expiredCoupons: Coupon[] = [ - // In past - { - amount: 10, exp: now, nbf: 0, schema: FixedAmountCouponSchema, currency: 'USD', - }, - // In future - { - percentage: 0.1, exp: now + 100_000_000, nbf: now + 10_000_000, schema: FixedPercentageCouponSchema, - }, - ] - it.each(expiredCoupons)('Does not apply coupons', async (coupon) => { - const terms = { ...termsBase, discounts: [await PayloadBuilder.dataHash(coupon)] } - const results = await sut.divine([terms, HUNDRED_DOLLAR_ESTIMATE, coupon]) - expect(results).toBeArrayOfSize(1) - const result = results.find(isDiscount) - expect(result).toBeDefined() - expect(result).toMatchObject({ amount: 0, schema: DiscountSchema }) - }) - }) -}) diff --git a/packages/payload/packages/payments/src/Discount/spec/tsconfig.json b/packages/payload/packages/payments/src/Discount/spec/tsconfig.json deleted file mode 100644 index 16980bd0b..000000000 --- a/packages/payload/packages/payments/src/Discount/spec/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@xylabs/tsconfig-jest" -} \ No newline at end of file diff --git a/packages/payloadset/packages/payments/package.json b/packages/payloadset/packages/payments/package.json index e89b98895..29cbe9f34 100644 --- a/packages/payloadset/packages/payments/package.json +++ b/packages/payloadset/packages/payments/package.json @@ -31,6 +31,13 @@ "dependencies": { "@xylabs/assert": "^4.0.9", "@xylabs/axios": "^4.0.9", + "@xylabs/exists": "^4.0.10", + "@xylabs/hex": "^4.0.10", + "@xyo-network/archivist-model": "^3.1.9", + "@xyo-network/diviner-abstract": "^3.1.9", + "@xyo-network/diviner-boundwitness-model": "^3.1.9", + "@xyo-network/diviner-hash-lease": "^3.1.9", + "@xyo-network/diviner-model": "^3.1.9", "@xyo-network/module-model": "^3.1.9", "@xyo-network/payload-builder": "^3.1.9", "@xyo-network/payload-model": "^3.1.9", @@ -38,6 +45,7 @@ "@xyo-network/rebilly-payment-payload-plugin": "workspace:^", "@xyo-network/sentinel-abstract": "^3.1.9", "@xyo-network/sentinel-model": "^3.1.9", + "@xyo-network/xns-record-payload-plugins": "workspace:^", "axios": "^1.7.7" }, "devDependencies": { diff --git a/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts b/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts index 2329fe071..555be655b 100644 --- a/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts +++ b/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts @@ -2,16 +2,12 @@ import type { Hash } from '@xylabs/hex' import type { DivinerInstance } from '@xyo-network/diviner-model' import { PayloadBuilder } from '@xyo-network/payload-builder' import type { Payload } from '@xyo-network/payload-model' -import type { EscrowTerms, Payment } from '@xyo-network/payment-payload-plugins' -import { PaymentSchema } from '@xyo-network/payment-payload-plugins' - -import type { Discount } from '../Discount/index.ts' -import { isDiscount } from '../Discount/index.ts' -import type { Subtotal } from '../Subtotal/index.ts' -import { isSubtotal } from '../Subtotal/index.ts' -import type { Total } from '../Total/index.ts' -import { isTotal } from '../Total/index.ts' -import type { Invoice } from './Invoice.ts' +import type { + Discount, EscrowTerms, Invoice, Payment, Subtotal, Total, +} from '@xyo-network/payment-payload-plugins' +import { + isDiscount, isSubtotal, isTotal, PaymentSchema, +} from '@xyo-network/payment-payload-plugins' /** * Validates the escrow terms to ensure they are valid for a purchase diff --git a/yarn.lock b/yarn.lock index ab4d3e67a..de6034d9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3909,6 +3909,13 @@ __metadata: languageName: node linkType: hard +"@xylabs/exists@npm:^4.0.10": + version: 4.0.10 + resolution: "@xylabs/exists@npm:4.0.10" + checksum: 10/2dade3d4ff6ee1842feb0e378e23ae214fd53ddf2cd399c65a05edfb0261de9b5eb8a280d654bf61172f41a7cbe19cd062fab69fee399b1d86fe22033677e743 + languageName: node + linkType: hard + "@xylabs/exists@npm:^4.0.9": version: 4.0.9 resolution: "@xylabs/exists@npm:4.0.9" @@ -3925,6 +3932,13 @@ __metadata: languageName: node linkType: hard +"@xylabs/hex@npm:^4.0.10": + version: 4.0.10 + resolution: "@xylabs/hex@npm:4.0.10" + checksum: 10/cad87986ced0e46e66c305d88dfd753772bd99701fe3b1fdbe97dee4d3ad05f7505dabbb1f5db9506c90caa69eeb8984b1031ebb5205c40839beb269982a5e73 + languageName: node + linkType: hard + "@xylabs/hex@npm:^4.0.9": version: 4.0.9 resolution: "@xylabs/hex@npm:4.0.9" @@ -7064,24 +7078,31 @@ __metadata: languageName: unknown linkType: soft -"@xyo-network/payment-payload-plugins@workspace:^, @xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments": +"@xyo-network/payment-payload-plugins@npm:^3.0.17, @xyo-network/payment-payload-plugins@workspace:^, @xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments": version: 0.0.0-use.local resolution: "@xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments" dependencies: "@xylabs/assert": "npm:^4.0.9" "@xylabs/crypto": "npm:^4.0.9" + "@xylabs/exists": "npm:^4.0.10" "@xylabs/hex": "npm:^4.0.9" "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" "@xylabs/tsconfig": "npm:^4.0.7" "@xyo-network/account-model": "npm:^3.1.9" + "@xyo-network/archivist-model": "npm:^3.1.9" "@xyo-network/boundwitness-builder": "npm:^3.1.9" "@xyo-network/boundwitness-model": "npm:^3.1.9" "@xyo-network/boundwitness-validator": "npm:^3.1.9" + "@xyo-network/diviner-abstract": "npm:^3.1.9" + "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/id-payload-plugin": "npm:^3.1.9" "@xyo-network/module-model": "npm:^3.1.9" + "@xyo-network/payload-builder": "npm:^3.1.9" "@xyo-network/payload-model": "npm:^3.1.9" + "@xyo-network/payment-payload-plugins": "npm:^3.0.17" + "@xyo-network/xns-record-payload-plugins": "workspace:^" typescript: "npm:^5.5.4" languageName: unknown linkType: soft @@ -7092,10 +7113,17 @@ __metadata: dependencies: "@xylabs/assert": "npm:^4.0.9" "@xylabs/axios": "npm:^4.0.9" + "@xylabs/exists": "npm:^4.0.10" + "@xylabs/hex": "npm:^4.0.10" "@xylabs/jest-helpers": "npm:^4.0.9" "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" "@xylabs/tsconfig": "npm:^4.0.7" + "@xyo-network/archivist-model": "npm:^3.1.9" "@xyo-network/boundwitness-model": "npm:^3.1.9" + "@xyo-network/diviner-abstract": "npm:^3.1.9" + "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" + "@xyo-network/diviner-hash-lease": "npm:^3.1.9" + "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/module-model": "npm:^3.1.9" "@xyo-network/payload-builder": "npm:^3.1.9" "@xyo-network/payload-model": "npm:^3.1.9" @@ -7103,6 +7131,7 @@ __metadata: "@xyo-network/rebilly-payment-payload-plugin": "workspace:^" "@xyo-network/sentinel-abstract": "npm:^3.1.9" "@xyo-network/sentinel-model": "npm:^3.1.9" + "@xyo-network/xns-record-payload-plugins": "workspace:^" axios: "npm:^1.7.7" jest: "npm:^29.7.0" typescript: "npm:^5.5.4" From ee150796a26fec67833846461b1572b5a87cbe9d Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 20:27:41 -0500 Subject: [PATCH 5/9] Update imports to use payload packages --- cspell.json | 1 + .../payload/packages/payments/package.json | 5 ----- .../payloadset/packages/payments/package.json | 14 ++++++------- .../payments/src/Discount/lib/applyCoupons.ts | 7 +++---- .../Discount/lib/spec/applyCoupons.spec.ts | 8 +++---- .../Invoice/spec/getInvoiceForEscrow.spec.ts | 7 +++---- .../packages/payments/src/Subtotal/Diviner.ts | 7 +++---- .../src/Subtotal/lib/appraisalValidators.ts | 2 +- .../src/Subtotal/spec/Diviner.spec.ts | 10 ++++----- .../packages/payments/src/Total/Diviner.ts | 21 ++++++++----------- .../payments/src/Total/spec/Diviner.spec.ts | 17 +++++++-------- yarn.lock | 19 +++++++---------- 12 files changed, 50 insertions(+), 68 deletions(-) diff --git a/cspell.json b/cspell.json index 2af5e6911..720991e7f 100644 --- a/cspell.json +++ b/cspell.json @@ -75,6 +75,7 @@ "scrollback", "Secp", "snyk", + "Stackable", "systeminfo", "systeminformation", "templatized", diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index f3db8e15d..e3c6512c4 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -31,22 +31,17 @@ "dependencies": { "@xylabs/assert": "^4.0.9", "@xylabs/crypto": "^4.0.9", - "@xylabs/exists": "^4.0.10", "@xylabs/hex": "^4.0.9", "@xyo-network/account-model": "^3.1.9", - "@xyo-network/archivist-model": "^3.1.9", "@xyo-network/boundwitness-builder": "^3.1.9", "@xyo-network/boundwitness-model": "^3.1.9", "@xyo-network/boundwitness-validator": "^3.1.9", - "@xyo-network/diviner-abstract": "^3.1.9", "@xyo-network/diviner-boundwitness-model": "^3.1.9", "@xyo-network/diviner-hash-lease": "^3.1.9", "@xyo-network/diviner-model": "^3.1.9", "@xyo-network/id-payload-plugin": "^3.1.9", "@xyo-network/module-model": "^3.1.9", - "@xyo-network/payload-builder": "^3.1.9", "@xyo-network/payload-model": "^3.1.9", - "@xyo-network/payment-payload-plugins": "^3.0.17", "@xyo-network/xns-record-payload-plugins": "workspace:^" }, "devDependencies": { diff --git a/packages/payloadset/packages/payments/package.json b/packages/payloadset/packages/payments/package.json index 29cbe9f34..c4dbfb72d 100644 --- a/packages/payloadset/packages/payments/package.json +++ b/packages/payloadset/packages/payments/package.json @@ -30,7 +30,6 @@ "types": "dist/neutral/index.d.ts", "dependencies": { "@xylabs/assert": "^4.0.9", - "@xylabs/axios": "^4.0.9", "@xylabs/exists": "^4.0.10", "@xylabs/hex": "^4.0.10", "@xyo-network/archivist-model": "^3.1.9", @@ -42,19 +41,20 @@ "@xyo-network/payload-builder": "^3.1.9", "@xyo-network/payload-model": "^3.1.9", "@xyo-network/payment-payload-plugins": "workspace:^", - "@xyo-network/rebilly-payment-payload-plugin": "workspace:^", - "@xyo-network/sentinel-abstract": "^3.1.9", - "@xyo-network/sentinel-model": "^3.1.9", "@xyo-network/xns-record-payload-plugins": "workspace:^", "axios": "^1.7.7" }, "devDependencies": { - "@xylabs/jest-helpers": "^4.0.9", "@xylabs/ts-scripts-yarn3": "^4.0.7", "@xylabs/tsconfig": "^4.0.7", - "@xyo-network/boundwitness-model": "^3.1.9", + "@xyo-network/account": "^3.1.9", + "@xyo-network/archivist-memory": "^3.1.9", + "@xyo-network/boundwitness-builder": "^3.1.9", + "@xyo-network/diviner-boundwitness-memory": "^3.1.9", + "@xyo-network/node-memory": "^3.1.9", "jest": "^29.7.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "vitest": "^2.0.5" }, "publishConfig": { "access": "public" diff --git a/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts b/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts index a079c7ea5..0ba84bae6 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts @@ -1,16 +1,15 @@ import { assertEx } from '@xylabs/assert' import { exists } from '@xylabs/exists' import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' - -import type { AmountFields } from '../../Amount/index.ts' import type { + AmountFields, Coupon, Discount, FixedAmountCoupon, FixedPercentageCoupon, -} from '../Payload/index.ts' +} from '@xyo-network/payment-payload-plugins' import { DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon, isStackable, -} from '../Payload/index.ts' +} from '@xyo-network/payment-payload-plugins' export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => { // Ensure all appraisals and coupons are in USD diff --git a/packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts b/packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts index 5a40c6a14..c46377ff8 100644 --- a/packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts +++ b/packages/payloadset/packages/payments/src/Discount/lib/spec/applyCoupons.spec.ts @@ -1,13 +1,13 @@ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' +import type { Coupon } from '@xyo-network/payment-payload-plugins' +import { + DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, +} from '@xyo-network/payment-payload-plugins' import { beforeEach, describe, it, vi, } from 'vitest' -import type { Coupon } from '../../Payload/index.ts' -import { - DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, -} from '../../Payload/index.ts' import { applyCoupons } from '../applyCoupons.ts' describe('applyCoupons', () => { diff --git a/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts index 04d2059c4..17814e508 100644 --- a/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts +++ b/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts @@ -3,12 +3,11 @@ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' import { MemoryNode } from '@xyo-network/node-memory' import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import type { EscrowTerms, PaymentTotalDivinerConfig } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema, NO_DISCOUNT } from '@xyo-network/payment-payload-plugins' -import { NO_DISCOUNT, PaymentDiscountDiviner } from '../../Discount/index.ts' +import { PaymentDiscountDiviner } from '../../Discount/index.ts' import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' -import type { PaymentTotalDivinerConfig } from '../../Total/index.ts' import { PaymentTotalDiviner } from '../../Total/index.ts' import { getInvoiceForEscrow } from '../getInvoiceForEscrow.ts' diff --git a/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts b/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts index 8491c1d9e..437d4a3a5 100644 --- a/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts @@ -4,14 +4,13 @@ import { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-mo import { creatableModule } from '@xyo-network/module-model' import { PayloadBuilder } from '@xyo-network/payload-builder' import { Payload } from '@xyo-network/payload-model' -import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' +import { + EscrowTerms, isEscrowTerms, PaymentSubtotalDivinerConfigSchema, PaymentSubtotalDivinerParams, Subtotal, SubtotalSchema, +} from '@xyo-network/payment-payload-plugins' -import { PaymentSubtotalDivinerConfigSchema } from './Config.ts' import { appraisalValidators, termsValidators, ValidEscrowTerms, } from './lib/index.ts' -import { PaymentSubtotalDivinerParams } from './Params.ts' -import { Subtotal, SubtotalSchema } from './Payload.ts' const currency = 'USD' diff --git a/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts index 94f009b80..5408b524e 100644 --- a/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts +++ b/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts @@ -1,6 +1,6 @@ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { isIso4217CurrencyCode } from '@xyo-network/payment-payload-plugins' -import { isIso4217CurrencyCode } from '../../Amount/index.ts' import { validateDuration } from './durationValidators.ts' const validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => { diff --git a/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts index b31c90e33..24df38c14 100644 --- a/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts +++ b/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts @@ -2,16 +2,16 @@ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' import { PayloadBuilder } from '@xyo-network/payload-builder' import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + EscrowTermsSchema, + isSubtotal, + SubtotalSchema, +} from '@xyo-network/payment-payload-plugins' import { beforeAll, beforeEach, describe, it, vi, } from 'vitest' import { PaymentSubtotalDiviner } from '../Diviner.ts' -import { - isSubtotal, - SubtotalSchema, -} from '../Payload.ts' describe('PaymentSubtotalDiviner', () => { let sut: PaymentSubtotalDiviner diff --git a/packages/payloadset/packages/payments/src/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Total/Diviner.ts index 9db4086c6..61db2cf4a 100644 --- a/packages/payloadset/packages/payments/src/Total/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Total/Diviner.ts @@ -5,18 +5,15 @@ import { asDivinerInstance, DivinerInstance, DivinerModuleEventData, } from '@xyo-network/diviner-model' import { creatableModule } from '@xyo-network/module-model' - import { Discount, - isDiscount, PaymentDiscountDiviner, - PaymentDiscountDivinerInputType, -} from '../Discount/index.ts' -import { - isSubtotal, PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType, Subtotal, -} from '../Subtotal/index.ts' -import { PaymentTotalDivinerConfigSchema } from './Config.ts' -import { PaymentTotalDivinerParams } from './Params.ts' -import { Total, TotalSchema } from './Payload.ts' + isDiscount, isDiscountWithMeta, isSubtotal, + isSubtotalWithMeta, + PaymentTotalDivinerConfigSchema, PaymentTotalDivinerParams, Subtotal, Total, TotalSchema, +} from '@xyo-network/payment-payload-plugins' + +import { PaymentDiscountDiviner, PaymentDiscountDivinerInputType } from '../Discount/index.ts' +import { PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType } from '../Subtotal/index.ts' type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType type OutputType = Subtotal | Discount | Total @@ -38,11 +35,11 @@ export class PaymentTotalDiviner< protected async divineHandler(payloads: TIn[] = []): Promise { const subtotalDiviner = await this.getPaymentSubtotalDiviner() const subtotalResult = await subtotalDiviner.divine(payloads) - const subtotal = subtotalResult.find(isSubtotal) + const subtotal = subtotalResult.find(isSubtotalWithMeta) if (!subtotal) return [] const discountDiviner = await this.getPaymentDiscountsDiviner() const discountResult = await discountDiviner.divine(payloads) - const discount = discountResult.find(isDiscount) + const discount = discountResult.find(isDiscountWithMeta) if (!discount) return [] const { currency: subtotalCurrency } = subtotal const { currency: discountCurrency } = discount diff --git a/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts index dbbce2ac6..7d418d0be 100644 --- a/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts +++ b/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts @@ -6,22 +6,19 @@ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' import { MemoryNode } from '@xyo-network/node-memory' import { PayloadBuilder } from '@xyo-network/payload-builder' -import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import type { Coupon, EscrowTerms } from '@xyo-network/payment-payload-plugins' +import { + EscrowTermsSchema, + FixedAmountCouponSchema, FixedPercentageCouponSchema, isTotal, + TotalSchema, +} from '@xyo-network/payment-payload-plugins' import { beforeAll, beforeEach, describe, it, vi, } from 'vitest' -import type { Coupon } from '../../Discount/index.ts' -import { - FixedAmountCouponSchema, FixedPercentageCouponSchema, PaymentDiscountDiviner, -} from '../../Discount/index.ts' +import { PaymentDiscountDiviner } from '../../Discount/index.ts' import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' import { PaymentTotalDiviner } from '../Diviner.ts' -import { - isTotal, - TotalSchema, -} from '../Payload.ts' describe('PaymentTotalDiviner', () => { let sut: PaymentTotalDiviner diff --git a/yarn.lock b/yarn.lock index de6034d9a..c5c8ac498 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7078,30 +7078,25 @@ __metadata: languageName: unknown linkType: soft -"@xyo-network/payment-payload-plugins@npm:^3.0.17, @xyo-network/payment-payload-plugins@workspace:^, @xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments": +"@xyo-network/payment-payload-plugins@workspace:^, @xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments": version: 0.0.0-use.local resolution: "@xyo-network/payment-payload-plugins@workspace:packages/payload/packages/payments" dependencies: "@xylabs/assert": "npm:^4.0.9" "@xylabs/crypto": "npm:^4.0.9" - "@xylabs/exists": "npm:^4.0.10" "@xylabs/hex": "npm:^4.0.9" "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" "@xylabs/tsconfig": "npm:^4.0.7" "@xyo-network/account-model": "npm:^3.1.9" - "@xyo-network/archivist-model": "npm:^3.1.9" "@xyo-network/boundwitness-builder": "npm:^3.1.9" "@xyo-network/boundwitness-model": "npm:^3.1.9" "@xyo-network/boundwitness-validator": "npm:^3.1.9" - "@xyo-network/diviner-abstract": "npm:^3.1.9" "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/id-payload-plugin": "npm:^3.1.9" "@xyo-network/module-model": "npm:^3.1.9" - "@xyo-network/payload-builder": "npm:^3.1.9" "@xyo-network/payload-model": "npm:^3.1.9" - "@xyo-network/payment-payload-plugins": "npm:^3.0.17" "@xyo-network/xns-record-payload-plugins": "workspace:^" typescript: "npm:^5.5.4" languageName: unknown @@ -7112,29 +7107,29 @@ __metadata: resolution: "@xyo-network/payment-plugin@workspace:packages/payloadset/packages/payments" dependencies: "@xylabs/assert": "npm:^4.0.9" - "@xylabs/axios": "npm:^4.0.9" "@xylabs/exists": "npm:^4.0.10" "@xylabs/hex": "npm:^4.0.10" - "@xylabs/jest-helpers": "npm:^4.0.9" "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" "@xylabs/tsconfig": "npm:^4.0.7" + "@xyo-network/account": "npm:^3.1.9" + "@xyo-network/archivist-memory": "npm:^3.1.9" "@xyo-network/archivist-model": "npm:^3.1.9" - "@xyo-network/boundwitness-model": "npm:^3.1.9" + "@xyo-network/boundwitness-builder": "npm:^3.1.9" "@xyo-network/diviner-abstract": "npm:^3.1.9" + "@xyo-network/diviner-boundwitness-memory": "npm:^3.1.9" "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/module-model": "npm:^3.1.9" + "@xyo-network/node-memory": "npm:^3.1.9" "@xyo-network/payload-builder": "npm:^3.1.9" "@xyo-network/payload-model": "npm:^3.1.9" "@xyo-network/payment-payload-plugins": "workspace:^" - "@xyo-network/rebilly-payment-payload-plugin": "workspace:^" - "@xyo-network/sentinel-abstract": "npm:^3.1.9" - "@xyo-network/sentinel-model": "npm:^3.1.9" "@xyo-network/xns-record-payload-plugins": "workspace:^" axios: "npm:^1.7.7" jest: "npm:^29.7.0" typescript: "npm:^5.5.4" + vitest: "npm:^2.0.5" languageName: unknown linkType: soft From 6a565a1abef99ea90f6783ead4ec2720b47a2683 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Tue, 10 Sep 2024 20:28:57 -0500 Subject: [PATCH 6/9] Cleanup imports --- .../packages/payments/src/Discount/Diviner.ts | 13 +++++-------- .../packages/payments/src/Total/Diviner.ts | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts index aeaedc751..0e0c4fe33 100644 --- a/packages/payloadset/packages/payments/src/Discount/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -14,18 +14,15 @@ import { import { creatableModule } from '@xyo-network/module-model' import { PayloadBuilder } from '@xyo-network/payload-builder' import { Payload } from '@xyo-network/payload-model' -import { EscrowTerms, isEscrowTerms } from '@xyo-network/payment-payload-plugins' - -import { PaymentDiscountDivinerConfigSchema } from './Config.ts' -import { applyCoupons } from './lib/index.ts' -import { PaymentDiscountDivinerParams } from './Params.ts' import { Coupon, Discount, - isCoupon, + EscrowTerms, isCoupon, isCouponWithMeta, - NO_DISCOUNT, -} from './Payload/index.ts' + isEscrowTerms, NO_DISCOUNT, PaymentDiscountDivinerConfigSchema, PaymentDiscountDivinerParams, +} from '@xyo-network/payment-payload-plugins' + +import { applyCoupons } from './lib/index.ts' const DEFAULT_BOUND_WITNESS_DIVINER_QUERY_PROPS: Readonly = { limit: 1, diff --git a/packages/payloadset/packages/payments/src/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Total/Diviner.ts index 61db2cf4a..f879cafc0 100644 --- a/packages/payloadset/packages/payments/src/Total/Diviner.ts +++ b/packages/payloadset/packages/payments/src/Total/Diviner.ts @@ -7,7 +7,7 @@ import { import { creatableModule } from '@xyo-network/module-model' import { Discount, - isDiscount, isDiscountWithMeta, isSubtotal, + isDiscountWithMeta, isSubtotalWithMeta, PaymentTotalDivinerConfigSchema, PaymentTotalDivinerParams, Subtotal, Total, TotalSchema, } from '@xyo-network/payment-payload-plugins' From 523eba62f49f0f2e77d6d5e6d3d36a488e22ca5a Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Wed, 11 Sep 2024 04:35:47 -0500 Subject: [PATCH 7/9] Update import paths --- .../payments/src/Discount/spec/Diviner.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts index c7e32b7d3..527f33a1a 100644 --- a/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts +++ b/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts @@ -7,18 +7,17 @@ import { HashLeaseEstimateSchema } from '@xyo-network/diviner-hash-lease' import { MemoryNode } from '@xyo-network/node-memory' import { PayloadBuilder } from '@xyo-network/payload-builder' import type { EscrowTerms } from '@xyo-network/payment-payload-plugins' -import { EscrowTermsSchema } from '@xyo-network/payment-payload-plugins' +import { + DiscountSchema, EscrowTermsSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, + isDiscount, + PaymentDiscountDivinerConfigSchema, +} from '@xyo-network/payment-payload-plugins' import { beforeEach, describe, it, vi, } from 'vitest' -import { PaymentDiscountDivinerConfigSchema } from '../Config.ts' import { PaymentDiscountDiviner } from '../Diviner.ts' import type { Coupon } from '../Payload/index.ts' -import { - DiscountSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, - isDiscount, -} from '../Payload/index.ts' describe('PaymentDiscountDiviner', () => { let sut: PaymentDiscountDiviner From c8d90b1986642da5ba5f4793e9a8c94ae4e195d2 Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Wed, 11 Sep 2024 04:39:29 -0500 Subject: [PATCH 8/9] Remove unused deps --- packages/payload/packages/payments/package.json | 1 - packages/payloadset/packages/payments/package.json | 3 +-- yarn.lock | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index e3c6512c4..5e4f72056 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -34,7 +34,6 @@ "@xylabs/hex": "^4.0.9", "@xyo-network/account-model": "^3.1.9", "@xyo-network/boundwitness-builder": "^3.1.9", - "@xyo-network/boundwitness-model": "^3.1.9", "@xyo-network/boundwitness-validator": "^3.1.9", "@xyo-network/diviner-boundwitness-model": "^3.1.9", "@xyo-network/diviner-hash-lease": "^3.1.9", diff --git a/packages/payloadset/packages/payments/package.json b/packages/payloadset/packages/payments/package.json index c4dbfb72d..d90638012 100644 --- a/packages/payloadset/packages/payments/package.json +++ b/packages/payloadset/packages/payments/package.json @@ -41,8 +41,7 @@ "@xyo-network/payload-builder": "^3.1.9", "@xyo-network/payload-model": "^3.1.9", "@xyo-network/payment-payload-plugins": "workspace:^", - "@xyo-network/xns-record-payload-plugins": "workspace:^", - "axios": "^1.7.7" + "@xyo-network/xns-record-payload-plugins": "workspace:^" }, "devDependencies": { "@xylabs/ts-scripts-yarn3": "^4.0.7", diff --git a/yarn.lock b/yarn.lock index c5c8ac498..0ad7c0e89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7089,7 +7089,6 @@ __metadata: "@xylabs/tsconfig": "npm:^4.0.7" "@xyo-network/account-model": "npm:^3.1.9" "@xyo-network/boundwitness-builder": "npm:^3.1.9" - "@xyo-network/boundwitness-model": "npm:^3.1.9" "@xyo-network/boundwitness-validator": "npm:^3.1.9" "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" @@ -7126,7 +7125,6 @@ __metadata: "@xyo-network/payload-model": "npm:^3.1.9" "@xyo-network/payment-payload-plugins": "workspace:^" "@xyo-network/xns-record-payload-plugins": "workspace:^" - axios: "npm:^1.7.7" jest: "npm:^29.7.0" typescript: "npm:^5.5.4" vitest: "npm:^2.0.5" From dc803c978a9528367fc0ce6c1471cd335c467e3b Mon Sep 17 00:00:00 2001 From: Joel Carter Date: Wed, 11 Sep 2024 05:05:34 -0500 Subject: [PATCH 9/9] Update deps --- packages/payload/packages/payments/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/payload/packages/payments/package.json b/packages/payload/packages/payments/package.json index 5e4f72056..7fbc4ce2f 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -34,8 +34,8 @@ "@xylabs/hex": "^4.0.9", "@xyo-network/account-model": "^3.1.9", "@xyo-network/boundwitness-builder": "^3.1.9", + "@xyo-network/boundwitness-model": "^3.1.9", "@xyo-network/boundwitness-validator": "^3.1.9", - "@xyo-network/diviner-boundwitness-model": "^3.1.9", "@xyo-network/diviner-hash-lease": "^3.1.9", "@xyo-network/diviner-model": "^3.1.9", "@xyo-network/id-payload-plugin": "^3.1.9", diff --git a/yarn.lock b/yarn.lock index 0ad7c0e89..05d6f3a19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7089,8 +7089,8 @@ __metadata: "@xylabs/tsconfig": "npm:^4.0.7" "@xyo-network/account-model": "npm:^3.1.9" "@xyo-network/boundwitness-builder": "npm:^3.1.9" + "@xyo-network/boundwitness-model": "npm:^3.1.9" "@xyo-network/boundwitness-validator": "npm:^3.1.9" - "@xyo-network/diviner-boundwitness-model": "npm:^3.1.9" "@xyo-network/diviner-hash-lease": "npm:^3.1.9" "@xyo-network/diviner-model": "npm:^3.1.9" "@xyo-network/id-payload-plugin": "npm:^3.1.9"