@xyo-network/payment-plugin 3.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +13 -0
  3. package/dist/neutral/Discount/Diviner.d.ts +19 -0
  4. package/dist/neutral/Discount/Diviner.d.ts.map +1 -0
  5. package/dist/neutral/Discount/index.d.ts +3 -0
  6. package/dist/neutral/Discount/index.d.ts.map +1 -0
  7. package/dist/neutral/Discount/lib/applyCoupons.d.ts +4 -0
  8. package/dist/neutral/Discount/lib/applyCoupons.d.ts.map +1 -0
  9. package/dist/neutral/Discount/lib/index.d.ts +2 -0
  10. package/dist/neutral/Discount/lib/index.d.ts.map +1 -0
  11. package/dist/neutral/Invoice/getInvoiceForEscrow.d.ts +6 -0
  12. package/dist/neutral/Invoice/getInvoiceForEscrow.d.ts.map +1 -0
  13. package/dist/neutral/Invoice/index.d.ts +2 -0
  14. package/dist/neutral/Invoice/index.d.ts.map +1 -0
  15. package/dist/neutral/Subtotal/Diviner.d.ts +12 -0
  16. package/dist/neutral/Subtotal/Diviner.d.ts.map +1 -0
  17. package/dist/neutral/Subtotal/index.d.ts +2 -0
  18. package/dist/neutral/Subtotal/index.d.ts.map +1 -0
  19. package/dist/neutral/Subtotal/lib/appraisalValidators.d.ts +3 -0
  20. package/dist/neutral/Subtotal/lib/appraisalValidators.d.ts.map +1 -0
  21. package/dist/neutral/Subtotal/lib/durationValidators.d.ts +3 -0
  22. package/dist/neutral/Subtotal/lib/durationValidators.d.ts.map +1 -0
  23. package/dist/neutral/Subtotal/lib/index.d.ts +3 -0
  24. package/dist/neutral/Subtotal/lib/index.d.ts.map +1 -0
  25. package/dist/neutral/Subtotal/lib/termsValidators.d.ts +4 -0
  26. package/dist/neutral/Subtotal/lib/termsValidators.d.ts.map +1 -0
  27. package/dist/neutral/Total/Diviner.d.ts +16 -0
  28. package/dist/neutral/Total/Diviner.d.ts.map +1 -0
  29. package/dist/neutral/Total/index.d.ts +2 -0
  30. package/dist/neutral/Total/index.d.ts.map +1 -0
  31. package/dist/neutral/index.d.ts +5 -0
  32. package/dist/neutral/index.d.ts.map +1 -0
  33. package/dist/neutral/index.mjs +347 -0
  34. package/dist/neutral/index.mjs.map +1 -0
  35. package/package.json +61 -0
  36. package/src/Discount/Diviner.ts +155 -0
  37. package/src/Discount/index.ts +2 -0
  38. package/src/Discount/lib/applyCoupons.ts +58 -0
  39. package/src/Discount/lib/index.ts +1 -0
  40. package/src/Invoice/getInvoiceForEscrow.ts +39 -0
  41. package/src/Invoice/index.ts +1 -0
  42. package/src/Subtotal/Diviner.ts +65 -0
  43. package/src/Subtotal/index.ts +1 -0
  44. package/src/Subtotal/lib/appraisalValidators.ts +45 -0
  45. package/src/Subtotal/lib/durationValidators.ts +18 -0
  46. package/src/Subtotal/lib/index.ts +2 -0
  47. package/src/Subtotal/lib/termsValidators.ts +18 -0
  48. package/src/Total/Diviner.ts +67 -0
  49. package/src/Total/index.ts +1 -0
  50. package/src/index.ts +4 -0
  51. package/typedoc.json +5 -0
  52. package/xy.config.ts +11 -0
@@ -0,0 +1,58 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { exists } from '@xylabs/exists'
3
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
4
+ import type {
5
+ AmountFields,
6
+ Coupon, Discount, FixedAmountCoupon,
7
+ FixedPercentageCoupon,
8
+ } from '@xyo-network/payment-payload-plugins'
9
+ import {
10
+ DiscountSchema, isFixedAmountCoupon, isFixedPercentageCoupon,
11
+ isStackable,
12
+ } from '@xyo-network/payment-payload-plugins'
13
+
14
+ export const applyCoupons = (appraisals: HashLeaseEstimate[], coupons: Coupon[]): Discount => {
15
+ // Ensure all appraisals and coupons are in USD
16
+ const allAppraisalsAreUSD = appraisals.every(appraisal => appraisal.currency === 'USD')
17
+ assertEx(allAppraisalsAreUSD, 'All appraisals must be in USD')
18
+ const allCouponsAreUSD = coupons.map(coupon => (coupon as Partial<AmountFields>)?.currency).filter(exists).every(currency => currency === 'USD')
19
+ assertEx(allCouponsAreUSD, 'All coupons must be in USD')
20
+ const total = appraisals.reduce((acc, appraisal) => acc + appraisal.price, 0)
21
+
22
+ // Calculated non-stackable discount coupons
23
+ const singularFixedDiscount = Math.max(...coupons
24
+ .filter(coupon => isFixedAmountCoupon(coupon) && !isStackable(coupon))
25
+ .map(coupon => (coupon as FixedAmountCoupon).amount), 0)
26
+ const singularPercentageDiscount = (Math.max(...coupons
27
+ .filter(coupon => isFixedPercentageCoupon(coupon) && !isStackable(coupon))
28
+ .map(coupon => (coupon as FixedPercentageCoupon).percentage), 0)) * total
29
+
30
+ // Calculate stackable discount coupons
31
+ // First calculate the total discount from fixed amount coupons
32
+ const stackedFixedDiscount = coupons
33
+ .filter(coupon => isFixedAmountCoupon(coupon) && isStackable(coupon))
34
+ .reduce((acc, coupon) => acc + (coupon as FixedAmountCoupon).amount, 0)
35
+ // Then calculate the total discount from percentage coupons and apply
36
+ // the percentage discount to the remaining total after fixed discounts
37
+ const stackedPercentageDiscount = coupons
38
+ .filter(coupon => isFixedPercentageCoupon(coupon) && isStackable(coupon))
39
+ .reduce((acc, coupon) => acc + (coupon as FixedPercentageCoupon).percentage, 0) * (total - stackedFixedDiscount)
40
+ // Sum all stackable discounts
41
+ const stackedDiscount = stackedFixedDiscount + stackedPercentageDiscount
42
+
43
+ // Find the best coupon(s) to apply
44
+ const maxDiscount = Math.max(
45
+ singularFixedDiscount,
46
+ singularPercentageDiscount,
47
+ stackedDiscount,
48
+ 0,
49
+ )
50
+
51
+ // Ensure discount is not more than the total
52
+ const amount = Math.min(maxDiscount, total)
53
+
54
+ // Return single discount payload
55
+ return {
56
+ amount, schema: DiscountSchema, currency: 'USD',
57
+ }
58
+ }
@@ -0,0 +1 @@
1
+ export * from './applyCoupons.ts'
@@ -0,0 +1,39 @@
1
+ import type { Hash } from '@xylabs/hex'
2
+ import type { DivinerInstance } from '@xyo-network/diviner-model'
3
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
4
+ import type { Payload } from '@xyo-network/payload-model'
5
+ import type {
6
+ Discount, EscrowTerms, Invoice, Payment, Subtotal, Total,
7
+ } from '@xyo-network/payment-payload-plugins'
8
+ import {
9
+ isDiscount, isSubtotal, isTotal, PaymentSchema,
10
+ } from '@xyo-network/payment-payload-plugins'
11
+
12
+ /**
13
+ * Validates the escrow terms to ensure they are valid for a purchase
14
+ * @returns A payment if the terms are valid for a purchase, undefined otherwise
15
+ */
16
+ export const getInvoiceForEscrow = async (
17
+ terms: EscrowTerms,
18
+ dataHashMap: Record<Hash, Payload>,
19
+ paymentTotalDiviner: DivinerInstance,
20
+ ): Promise<Invoice | undefined> => {
21
+ const payloads = Object.values(dataHashMap)
22
+ const results = await paymentTotalDiviner.divine([terms, ...payloads])
23
+ const subtotal = results.find(isSubtotal) as Subtotal | undefined
24
+ const discount = results.find(isDiscount) as Discount | undefined
25
+ const total = results.find(isTotal) as Total | undefined
26
+ if (!subtotal || !total) return undefined
27
+ const { amount, currency } = total
28
+ if (currency !== 'USD') return undefined
29
+ const sources = await getSources(terms, subtotal, total, discount)
30
+ const payment: Payment = {
31
+ amount, currency, schema: PaymentSchema, sources,
32
+ }
33
+ return discount ? [subtotal, total, payment, discount] : [subtotal, total, payment]
34
+ }
35
+
36
+ const getSources = async (terms: EscrowTerms, subtotal: Subtotal, total: Total, discount?: Discount): Promise<Hash[]> => {
37
+ const sources = discount ? [terms, subtotal, total, discount] : [terms, subtotal, total]
38
+ return await Promise.all(sources.map(p => PayloadBuilder.dataHash(p)))
39
+ }
@@ -0,0 +1 @@
1
+ export * from './getInvoiceForEscrow.ts'
@@ -0,0 +1,65 @@
1
+ import { AbstractDiviner } from '@xyo-network/diviner-abstract'
2
+ import { HashLeaseEstimate, isHashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
3
+ import { DivinerInstance, DivinerModuleEventData } from '@xyo-network/diviner-model'
4
+ import { creatableModule } from '@xyo-network/module-model'
5
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
6
+ import { Payload } from '@xyo-network/payload-model'
7
+ import {
8
+ EscrowTerms, isEscrowTerms, PaymentSubtotalDivinerConfigSchema, PaymentSubtotalDivinerParams, Subtotal, SubtotalSchema,
9
+ } from '@xyo-network/payment-payload-plugins'
10
+
11
+ import {
12
+ appraisalValidators, termsValidators, ValidEscrowTerms,
13
+ } from './lib/index.ts'
14
+
15
+ const currency = 'USD'
16
+
17
+ /**
18
+ * Escrow terms that contain all the valid fields for calculating a subtotal
19
+ */
20
+ export type PaymentSubtotalDivinerInputType = EscrowTerms | HashLeaseEstimate | Payload
21
+
22
+ @creatableModule()
23
+ export class PaymentSubtotalDiviner<
24
+ TParams extends PaymentSubtotalDivinerParams = PaymentSubtotalDivinerParams,
25
+ TIn extends PaymentSubtotalDivinerInputType = PaymentSubtotalDivinerInputType,
26
+ TOut extends Subtotal = Subtotal,
27
+ TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<
28
+ DivinerInstance<TParams, TIn, TOut>,
29
+ TIn,
30
+ TOut
31
+ >,
32
+ > extends AbstractDiviner<TParams, TIn, TOut, TEventData> {
33
+ static override configSchemas = [PaymentSubtotalDivinerConfigSchema]
34
+ static override defaultConfigSchema = PaymentSubtotalDivinerConfigSchema
35
+
36
+ protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {
37
+ // Find the escrow terms
38
+ const terms = payloads.find(isEscrowTerms) as EscrowTerms | undefined
39
+ if (!terms) return []
40
+
41
+ // Run all terms validations
42
+ if (!termsValidators.every(validator => validator(terms))) return []
43
+ const validTerms = terms as ValidEscrowTerms
44
+
45
+ // Retrieve all appraisals from terms
46
+ const hashMap = await PayloadBuilder.toAllHashMap(payloads)
47
+ const appraisals = validTerms.appraisals.map(appraisal => hashMap[appraisal]).filter(isHashLeaseEstimate) as unknown as HashLeaseEstimate[]
48
+
49
+ // Ensure all appraisals are present
50
+ if (appraisals.length !== validTerms.appraisals.length) return []
51
+
52
+ // Run all appraisal validations
53
+ if (!appraisalValidators.every(validator => validator(appraisals))) return []
54
+ const amount = calculateSubtotal(appraisals)
55
+ const sources = [await PayloadBuilder.dataHash(validTerms), ...validTerms.appraisals]
56
+ return [{
57
+ amount, currency, schema: SubtotalSchema, sources,
58
+ }] as TOut[]
59
+ }
60
+ }
61
+
62
+ // TODO: Add support for other currencies
63
+ const calculateSubtotal = (appraisals: HashLeaseEstimate[]): number => {
64
+ return appraisals.reduce((sum, appraisal) => sum + appraisal.price, 0)
65
+ }
@@ -0,0 +1 @@
1
+ export * from './Diviner.ts'
@@ -0,0 +1,45 @@
1
+ import type { HashLeaseEstimate } from '@xyo-network/diviner-hash-lease'
2
+ import { isIso4217CurrencyCode } from '@xyo-network/payment-payload-plugins'
3
+
4
+ import { validateDuration } from './durationValidators.ts'
5
+
6
+ const validateAppraisalAmount = (appraisals: HashLeaseEstimate[]): boolean => {
7
+ // Ensure all appraisals are numeric
8
+ if (appraisals.some(appraisal => typeof appraisal.price !== 'number')) return false
9
+ // Ensure all appraisals are positive numbers
10
+ if (appraisals.some(appraisal => appraisal.price < 0)) return false
11
+ return true
12
+ }
13
+
14
+ const validateAppraisalCurrency = (appraisals: HashLeaseEstimate[]): boolean => {
15
+ // NOTE: Only supporting USD for now, the remaining checks are for future-proofing.
16
+ if (!appraisals.every(appraisal => appraisal.currency == 'USD')) return false
17
+
18
+ // Check every object in the array to ensure they all are in a supported currency.
19
+ if (!appraisals.every(appraisal => isIso4217CurrencyCode(appraisal.currency))) return false
20
+
21
+ return true
22
+ }
23
+
24
+ const validateAppraisalConsistentCurrency = (appraisals: HashLeaseEstimate[]): boolean => {
25
+ // Check if the array is empty or contains only one element, no need to compare.
26
+ if (appraisals.length <= 1) return true
27
+
28
+ // Get the currency of the first element to compare with others.
29
+ const { currency } = appraisals[0]
30
+ if (!currency) return false
31
+
32
+ // Check every object in the array to ensure they all have the same currency.
33
+ if (!appraisals.every(item => item.currency === currency)) return false
34
+
35
+ return true
36
+ }
37
+
38
+ const validateAppraisalWindow = (appraisals: HashLeaseEstimate[]): boolean => appraisals.every(validateDuration)
39
+
40
+ export const appraisalValidators = [
41
+ validateAppraisalAmount,
42
+ validateAppraisalCurrency,
43
+ validateAppraisalConsistentCurrency,
44
+ validateAppraisalWindow,
45
+ ]
@@ -0,0 +1,18 @@
1
+ import type { DurationFields } from '@xyo-network/xns-record-payload-plugins'
2
+
3
+ const FIVE_MINUTES = 1000 * 60 * 5
4
+
5
+ /**
6
+ * Validates that the current time is within the duration window, within a configurable a buffer
7
+ * @param value The duration value
8
+ * @param windowMs The window in milliseconds to allow for a buffer
9
+ * @returns True if the duration is valid, false otherwise
10
+ */
11
+ export const validateDuration = (value: Partial<DurationFields>, windowMs = FIVE_MINUTES): boolean => {
12
+ const now = Date.now()
13
+ if (!value.nbf || value.nbf > now) return false
14
+ // If already expired (include for a 5 minute buffer to allow for a reasonable
15
+ // minimum amount of time for the transaction to be processed)
16
+ if (!value.exp || value.exp - now < windowMs) return false
17
+ return true
18
+ }
@@ -0,0 +1,2 @@
1
+ export * from './appraisalValidators.ts'
2
+ export * from './termsValidators.ts'
@@ -0,0 +1,18 @@
1
+ import type { Hash } from '@xylabs/hex'
2
+ import type { EscrowTerms } from '@xyo-network/payment-payload-plugins'
3
+
4
+ import { validateDuration } from './durationValidators.ts'
5
+
6
+ export type ValidEscrowTerms = Required<EscrowTerms>
7
+
8
+ const validateTermsAppraisals = (terms: EscrowTerms): terms is Required<EscrowTerms & { appraisals: Hash[] }> => {
9
+ if (!terms.appraisals) return false
10
+ if (terms.appraisals.length === 0) return false
11
+ return true
12
+ }
13
+ const validateTermsWindow = (terms: EscrowTerms): boolean => validateDuration(terms)
14
+
15
+ export const termsValidators = [
16
+ validateTermsAppraisals,
17
+ validateTermsWindow,
18
+ ]
@@ -0,0 +1,67 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { Hash } from '@xylabs/hex'
3
+ import { AbstractDiviner } from '@xyo-network/diviner-abstract'
4
+ import {
5
+ asDivinerInstance, DivinerInstance, DivinerModuleEventData,
6
+ } from '@xyo-network/diviner-model'
7
+ import { creatableModule } from '@xyo-network/module-model'
8
+ import {
9
+ Discount,
10
+ isDiscountWithMeta,
11
+ isSubtotalWithMeta,
12
+ PaymentTotalDivinerConfigSchema, PaymentTotalDivinerParams, Subtotal, Total, TotalSchema,
13
+ } from '@xyo-network/payment-payload-plugins'
14
+
15
+ import { PaymentDiscountDiviner, PaymentDiscountDivinerInputType } from '../Discount/index.ts'
16
+ import { PaymentSubtotalDiviner, PaymentSubtotalDivinerInputType } from '../Subtotal/index.ts'
17
+
18
+ type InputType = PaymentDiscountDivinerInputType | PaymentSubtotalDivinerInputType
19
+ type OutputType = Subtotal | Discount | Total
20
+
21
+ @creatableModule()
22
+ export class PaymentTotalDiviner<
23
+ TParams extends PaymentTotalDivinerParams = PaymentTotalDivinerParams,
24
+ TIn extends InputType = InputType,
25
+ TOut extends OutputType = OutputType,
26
+ TEventData extends DivinerModuleEventData<DivinerInstance<TParams, TIn, TOut>, TIn, TOut> = DivinerModuleEventData<
27
+ DivinerInstance<TParams, TIn, TOut>,
28
+ TIn,
29
+ TOut
30
+ >,
31
+ > extends AbstractDiviner<TParams, TIn, TOut, TEventData> {
32
+ static override configSchemas = [PaymentTotalDivinerConfigSchema]
33
+ static override defaultConfigSchema: PaymentTotalDivinerConfigSchema = PaymentTotalDivinerConfigSchema
34
+
35
+ protected async divineHandler(payloads: TIn[] = []): Promise<TOut[]> {
36
+ const subtotalDiviner = await this.getPaymentSubtotalDiviner()
37
+ const subtotalResult = await subtotalDiviner.divine(payloads)
38
+ const subtotal = subtotalResult.find(isSubtotalWithMeta)
39
+ if (!subtotal) return []
40
+ const discountDiviner = await this.getPaymentDiscountsDiviner()
41
+ const discountResult = await discountDiviner.divine(payloads)
42
+ const discount = discountResult.find(isDiscountWithMeta)
43
+ if (!discount) return []
44
+ const { currency: subtotalCurrency } = subtotal
45
+ const { currency: discountCurrency } = discount
46
+ assertEx(subtotalCurrency === discountCurrency, () => `Subtotal currency ${subtotalCurrency} does not match discount currency ${discountCurrency}`)
47
+ const amount = Math.max(0, subtotal.amount - discount.amount)
48
+ const currency = subtotalCurrency
49
+ const sources = [subtotal.$hash, discount.$hash] as Hash[]
50
+ const total: Total = {
51
+ amount, currency, sources, schema: TotalSchema,
52
+ }
53
+ return [subtotal, discount, total] as TOut[]
54
+ }
55
+
56
+ protected async getPaymentDiscountsDiviner(): Promise<PaymentDiscountDiviner> {
57
+ const name = assertEx(this.config.paymentDiscountDiviner, () => 'Missing paymentDiscountDiviner in config')
58
+ const mod = assertEx(await this.resolve(name), () => `Error resolving paymentDiscountDiviner: ${name}`)
59
+ return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentDiscountDiviner
60
+ }
61
+
62
+ protected async getPaymentSubtotalDiviner(): Promise<PaymentSubtotalDiviner> {
63
+ const name = assertEx(this.config.paymentSubtotalDiviner, () => 'Missing paymentSubtotalDiviner in config')
64
+ const mod = assertEx(await this.resolve(name), () => `Error resolving paymentSubtotalDiviner: ${name}`)
65
+ return assertEx(asDivinerInstance(mod), () => `Resolved module ${mod.address} not a valid Diviner`) as PaymentSubtotalDiviner
66
+ }
67
+ }
@@ -0,0 +1 @@
1
+ export * from './Diviner.ts'
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './Discount/index.ts'
2
+ export * from './Invoice/index.ts'
3
+ export * from './Subtotal/index.ts'
4
+ export * from './Total/index.ts'
package/typedoc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://typedoc.org/schema.json",
3
+ "entryPoints": ["./src/index.ts"],
4
+ "tsconfig": "./tsconfig.typedoc.json"
5
+ }
package/xy.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { XyTsupConfig } from '@xylabs/ts-scripts-yarn3'
2
+ const config: XyTsupConfig = {
3
+ compile: {
4
+ browser: {},
5
+ node: {},
6
+ neutral: { src: true },
7
+ },
8
+ }
9
+
10
+ // eslint-disable-next-line import-x/no-default-export
11
+ export default config