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 37767b314..7fbc4ce2f 100644 --- a/packages/payload/packages/payments/package.json +++ b/packages/payload/packages/payments/package.json @@ -37,9 +37,11 @@ "@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" + "@xyo-network/payload-model": "^3.1.9", + "@xyo-network/xns-record-payload-plugins": "workspace:^" }, "devDependencies": { "@xylabs/ts-scripts-yarn3": "^4.0.7", 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/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..905e8a429 --- /dev/null +++ b/packages/payload/packages/payments/src/Discount/index.ts @@ -0,0 +1,3 @@ +export * from './Config.ts' +export * from './Params.ts' +export * from './Payload/index.ts' 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..d07acd589 --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/Invoice.ts @@ -0,0 +1,19 @@ +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' + +/** + * 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/index.ts b/packages/payload/packages/payments/src/Invoice/index.ts new file mode 100644 index 000000000..faed65398 --- /dev/null +++ b/packages/payload/packages/payments/src/Invoice/index.ts @@ -0,0 +1 @@ +export * from './Invoice.ts' diff --git a/packages/payload/packages/payments/src/Subtotal/Diviner/Config.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Config.ts new file mode 100644 index 000000000..8eb263689 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/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/Params.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Params.ts new file mode 100644 index 000000000..7e05fbc3a --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/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/Diviner/Payload.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/Payload.ts new file mode 100644 index 000000000..5703513b6 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/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/Diviner/index.ts b/packages/payload/packages/payments/src/Subtotal/Diviner/index.ts new file mode 100644 index 000000000..ccda19e2f --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/Diviner/index.ts @@ -0,0 +1,3 @@ +export * from './Config.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 new file mode 100644 index 000000000..e57fdbc61 --- /dev/null +++ b/packages/payload/packages/payments/src/Subtotal/index.ts @@ -0,0 +1 @@ +export * from './Diviner/index.ts' diff --git a/packages/payload/packages/payments/src/Total/Diviner/Config.ts b/packages/payload/packages/payments/src/Total/Diviner/Config.ts new file mode 100644 index 000000000..143770b1a --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Diviner/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/Params.ts b/packages/payload/packages/payments/src/Total/Diviner/Params.ts new file mode 100644 index 000000000..07942f3ba --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Diviner/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/Diviner/Payload.ts b/packages/payload/packages/payments/src/Total/Diviner/Payload.ts new file mode 100644 index 000000000..24304da28 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Diviner/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/Diviner/index.ts b/packages/payload/packages/payments/src/Total/Diviner/index.ts new file mode 100644 index 000000000..ccda19e2f --- /dev/null +++ b/packages/payload/packages/payments/src/Total/Diviner/index.ts @@ -0,0 +1,3 @@ +export * from './Config.ts' +export * from './Params.ts' +export * from './Payload.ts' 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..e57fdbc61 --- /dev/null +++ b/packages/payload/packages/payments/src/Total/index.ts @@ -0,0 +1 @@ +export * from './Diviner/index.ts' 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..d90638012 --- /dev/null +++ b/packages/payloadset/packages/payments/package.json @@ -0,0 +1,61 @@ +{ + "name": "@xyo-network/payment-plugin", + "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/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", + "@xyo-network/payment-payload-plugins": "workspace:^", + "@xyo-network/xns-record-payload-plugins": "workspace:^" + }, + "devDependencies": { + "@xylabs/ts-scripts-yarn3": "^4.0.7", + "@xylabs/tsconfig": "^4.0.7", + "@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", + "vitest": "^2.0.5" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/payloadset/packages/payments/src/Discount/Diviner.ts b/packages/payloadset/packages/payments/src/Discount/Diviner.ts new file mode 100644 index 000000000..0e0c4fe33 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Discount/Diviner.ts @@ -0,0 +1,155 @@ +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 { + Coupon, + Discount, + EscrowTerms, isCoupon, + isCouponWithMeta, + 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, + 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/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/Discount/lib/applyCoupons.ts b/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts new file mode 100644 index 000000000..0ba84bae6 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Discount/lib/applyCoupons.ts @@ -0,0 +1,58 @@ +import { assertEx } from '@xylabs/assert' +import { exists } from '@xylabs/exists' +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import type { + AmountFields, + Coupon, Discount, FixedAmountCoupon, + FixedPercentageCoupon, +} from '@xyo-network/payment-payload-plugins' +import { + DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon, + isStackable, +} from '@xyo-network/payment-payload-plugins' + +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/Discount/lib/index.ts b/packages/payloadset/packages/payments/src/Discount/lib/index.ts new file mode 100644 index 000000000..89c610e91 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Discount/lib/index.ts @@ -0,0 +1 @@ +export * from './applyCoupons.ts' 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 new file mode 100644 index 000000000..c46377ff8 --- /dev/null +++ b/packages/payloadset/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 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 { 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/Discount/lib/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Discount/lib/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts new file mode 100644 index 000000000..527f33a1a --- /dev/null +++ b/packages/payloadset/packages/payments/src/Discount/spec/Diviner.spec.ts @@ -0,0 +1,126 @@ +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 { + DiscountSchema, EscrowTermsSchema, FixedAmountCouponSchema, FixedPercentageCouponSchema, + isDiscount, + PaymentDiscountDivinerConfigSchema, +} from '@xyo-network/payment-payload-plugins' +import { + beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentDiscountDiviner } from '../Diviner.ts' +import type { Coupon } 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/Discount/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Discount/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts b/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts new file mode 100644 index 000000000..555be655b --- /dev/null +++ b/packages/payloadset/packages/payments/src/Invoice/getInvoiceForEscrow.ts @@ -0,0 +1,39 @@ +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, 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 + * @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/Invoice/index.ts b/packages/payloadset/packages/payments/src/Invoice/index.ts new file mode 100644 index 000000000..cc3b200e8 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Invoice/index.ts @@ -0,0 +1 @@ +export * from './getInvoiceForEscrow.ts' diff --git a/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts b/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts new file mode 100644 index 000000000..17814e508 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Invoice/spec/getInvoiceForEscrow.spec.ts @@ -0,0 +1,68 @@ +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, PaymentTotalDivinerConfig } from '@xyo-network/payment-payload-plugins' +import { EscrowTermsSchema, NO_DISCOUNT } from '@xyo-network/payment-payload-plugins' + +import { PaymentDiscountDiviner } from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/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/Invoice/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Invoice/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Subtotal/Diviner.ts b/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts new file mode 100644 index 000000000..437d4a3a5 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Subtotal/Diviner.ts @@ -0,0 +1,65 @@ +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, PaymentSubtotalDivinerConfigSchema, PaymentSubtotalDivinerParams, Subtotal, SubtotalSchema, +} from '@xyo-network/payment-payload-plugins' + +import { + appraisalValidators, termsValidators, ValidEscrowTerms, +} from './lib/index.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/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/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts new file mode 100644 index 000000000..5408b524e --- /dev/null +++ b/packages/payloadset/packages/payments/src/Subtotal/lib/appraisalValidators.ts @@ -0,0 +1,45 @@ +import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease' +import { isIso4217CurrencyCode } from '@xyo-network/payment-payload-plugins' + +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/Subtotal/lib/durationValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/durationValidators.ts new file mode 100644 index 000000000..52831e574 --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Subtotal/lib/index.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/index.ts new file mode 100644 index 000000000..c454d02eb --- /dev/null +++ b/packages/payloadset/packages/payments/src/Subtotal/lib/index.ts @@ -0,0 +1,2 @@ +export * from './appraisalValidators.ts' +export * from './termsValidators.ts' diff --git a/packages/payloadset/packages/payments/src/Subtotal/lib/termsValidators.ts b/packages/payloadset/packages/payments/src/Subtotal/lib/termsValidators.ts new file mode 100644 index 000000000..c447a824b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Subtotal/spec/Diviner.spec.ts new file mode 100644 index 000000000..24df38c14 --- /dev/null +++ b/packages/payloadset/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, + isSubtotal, + SubtotalSchema, +} from '@xyo-network/payment-payload-plugins' +import { + beforeAll, beforeEach, describe, it, vi, +} from 'vitest' + +import { PaymentSubtotalDiviner } from '../Diviner.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/Subtotal/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Subtotal/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/Total/Diviner.ts b/packages/payloadset/packages/payments/src/Total/Diviner.ts new file mode 100644 index 000000000..f879cafc0 --- /dev/null +++ b/packages/payloadset/packages/payments/src/Total/Diviner.ts @@ -0,0 +1,67 @@ +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, + isDiscountWithMeta, + 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 + +@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(isSubtotalWithMeta) + if (!subtotal) return [] + const discountDiviner = await this.getPaymentDiscountsDiviner() + const discountResult = await discountDiviner.divine(payloads) + const discount = discountResult.find(isDiscountWithMeta) + 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/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/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts b/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts new file mode 100644 index 000000000..7d418d0be --- /dev/null +++ b/packages/payloadset/packages/payments/src/Total/spec/Diviner.spec.ts @@ -0,0 +1,170 @@ +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 { 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 { PaymentDiscountDiviner } from '../../Discount/index.ts' +import { PaymentSubtotalDiviner } from '../../Subtotal/index.ts' +import { PaymentTotalDiviner } from '../Diviner.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/Total/spec/tsconfig.json b/packages/payloadset/packages/payments/src/Total/spec/tsconfig.json new file mode 100644 index 000000000..16980bd0b --- /dev/null +++ b/packages/payloadset/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/payloadset/packages/payments/src/index.ts b/packages/payloadset/packages/payments/src/index.ts new file mode 100644 index 000000000..d8691959e --- /dev/null +++ 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/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 diff --git a/yarn.lock b/yarn.lock index 09b28a859..05d6f3a19 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" @@ -7078,10 +7092,42 @@ __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" + "@xyo-network/xns-record-payload-plugins": "workspace:^" + typescript: "npm:^5.5.4" + 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/exists": "npm:^4.0.10" + "@xylabs/hex": "npm:^4.0.10" + "@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-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/xns-record-payload-plugins": "workspace:^" + jest: "npm:^29.7.0" typescript: "npm:^5.5.4" + vitest: "npm:^2.0.5" languageName: unknown linkType: soft