@better-giving/donation 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@better-giving/donation",
3
+ "version": "1.0.0",
4
+ "devDependencies": {
5
+ "@better-giving/config": "workspace:*"
6
+ },
7
+ "peerDependencies": {
8
+ "@better-giving/types": "1.0.1",
9
+ "valibot": "0.42.0"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": "./dist/donation.mjs",
17
+ "./*": "./dist/*.mjs"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc --outDir dist",
21
+ "publish": "npm publish --access public"
22
+ }
23
+ }
@@ -0,0 +1,535 @@
1
+ import type { ISODate } from "@better-giving/types/alias";
2
+ import type { ChainID, Environment } from "@better-giving/types/list";
3
+ import type { EmptyObject, OrStr } from "@better-giving/types/utils";
4
+
5
+ export declare namespace Donation {
6
+ type Index =
7
+ | "AppUsed_Index"
8
+ | "Checked-8283_Index"
9
+ | "Email_Index"
10
+ | "EndowmentId_Index"
11
+ | "FundDeposits_Index"
12
+ | "Tax-Receipt-Sent_Index"
13
+ | "Network-FinalizedDate_Index"
14
+ | "WalletAddress_Index";
15
+
16
+ type App =
17
+ | "aging"
18
+ | "angel-protocol"
19
+ | "bg-marketplace"
20
+ | "bg-widget"
21
+ | "black-history-month"
22
+ | "make-whole"
23
+ | "mental-health"
24
+ | "restore-earth"
25
+ | "tester-app"
26
+ | "ukraine-portal";
27
+
28
+ type Source = Extract<
29
+ Donation.App,
30
+ "bg-marketplace" | "bg-widget" | "tester-app"
31
+ >;
32
+
33
+ type Client = "apes" | "normal";
34
+
35
+ type ApesComplianceReviewStatus = "pending" | "approved";
36
+
37
+ type PrimaryKey = {
38
+ transactionId: string; //PK
39
+ };
40
+ /** AppUsed_Index */
41
+ type AppUsed_IndexKey = {
42
+ appUsed?: App; //PK
43
+ };
44
+
45
+ /** Checked-8283_Index */
46
+ type Checked_8283_IndexKey = {
47
+ checked8283?: "yes" | "no"; //PK
48
+ };
49
+
50
+ /** Email_Index */
51
+ type EmailIndexKey = {
52
+ /** donor email */
53
+ email?: string; //PK
54
+ };
55
+
56
+ /** EndowmentId_Index */
57
+ type EndowmentId_IndexKey = {
58
+ endowmentId?: number; //PK
59
+ };
60
+
61
+ type FiatRamp_IndexKey = {
62
+ fiatRamp?: "STRIPE" | "PAYPAL" | "CHARIOT";
63
+ };
64
+
65
+ /** FundDeposits_Index */
66
+ type FundDeposits_IndexKey = {
67
+ fundDepositTxHash?: string; //PK
68
+ donationFinalTxHash?: string; //SK
69
+ };
70
+
71
+ /** Network-FinalizedDate_Index */
72
+ type Network_FinalizedDate_IndexKey = {
73
+ network: Environment; //PK
74
+ donationFinalTxDate: ISODate; //SK
75
+ };
76
+
77
+ /** Tax-Receipt-Sent_Index */
78
+ type Tax_Receipt_Sent_IndexKey = {
79
+ taxReceiptSent?: "yes" | "no"; //PK
80
+ };
81
+
82
+ /** WalletAddress_Index */
83
+ type WalletAddress_Index = {
84
+ walletAddress?: string; //PK
85
+ };
86
+
87
+ type DonorTitle = "Mr" | "Ms" | "Mrs" | "Mx";
88
+ type DonorBasicInfo = {
89
+ /** may be empty */
90
+ title?: DonorTitle;
91
+ fullName: string;
92
+ kycEmail: string;
93
+ /** signifies eligibility to UK Gift Aid, defaults to `false` for crypto donations */
94
+ ukGiftAid: boolean;
95
+ };
96
+ type DonorAddress = {
97
+ streetAddress: string;
98
+ /** may be empty `''` */
99
+ city: string;
100
+ /** optional */
101
+ state?: string;
102
+ zipCode: string;
103
+ country: string;
104
+ };
105
+
106
+ type V2FullKyc = DonorBasicInfo & DonorAddress;
107
+ type V2Kyc = DonorBasicInfo | V2FullKyc;
108
+ type PartialV2Kyc = Partial<V2FullKyc>;
109
+
110
+ type V2NonKeyAttributes = {
111
+ /** program which the donation is attributed: may be empty `""` */
112
+ programId?: string;
113
+ programName?: string;
114
+ /** honorary full name - may be empty `""` */
115
+ inHonorOf?: string;
116
+ transactionDate: ISODate;
117
+ /** if tipAmount is present, value includes tipAmount */
118
+ amount: number;
119
+ /** USD value at the time of donation */
120
+ usdValue: number;
121
+ /**
122
+ * settled USD amount before fsa,base fees
123
+ *
124
+ * Crypto: post-swaps USDC amount and converted to USD equivalent
125
+ *
126
+ * Fiat: 1:1 for USD donations while post-FX conversion for non USD donations
127
+ */
128
+ settledUsdAmount: number;
129
+ feeAllowance: number;
130
+ /** what's left of feeAllowance after covering processing fee, could be less than 0 (loss) */
131
+ excessFeeAllowanceUsd: number;
132
+ /** indicates DB record is not yet separated into endow-record & tip-record */
133
+ tipAmount?: number;
134
+ /** indicates this is a tip record extracted from parent */
135
+ parentTx?: string;
136
+
137
+ denomination: string;
138
+ chainId: ChainID.V2SupportedChainID;
139
+ chainName: string;
140
+ /** 0 - 100 */
141
+ splitLiq: string;
142
+
143
+ charityName: string;
144
+ nonProfitMsg?: string;
145
+ fiscalSponsored: boolean;
146
+ client: Client;
147
+ network: Environment;
148
+ paymentMethod: string;
149
+ isRecurring: boolean;
150
+
151
+ //finalized
152
+ destinationChainId: OrStr<ChainID.V2DestinationChainID>;
153
+ donationFinalAmount: number;
154
+ donationFinalChainId: OrStr<ChainID.V2DestinationChainID>;
155
+ donationFinalDenom: string;
156
+ donationFinalTxDate: ISODate;
157
+ donationFinalTxHash: string;
158
+
159
+ //fees
160
+ baseFee: number;
161
+ processingFee: number;
162
+ fiscalSponsorFee: number;
163
+
164
+ /// fundraiser this donation is attributed to
165
+ fund_id?: string;
166
+ fund_name?: string;
167
+ fund_tx?: string;
168
+ };
169
+
170
+ type NonKeyAttributes = {
171
+ addressComplement?: " ";
172
+ /** one record with this value */
173
+ apesComplianceNewRecipient?: 4;
174
+ apesComplianceReviewStatus?: ApesComplianceReviewStatus;
175
+ apesComplianceTimeWindow?: ISODate;
176
+ /** source chainID */
177
+ chainId?:
178
+ | ChainID.V2SupportedChainID
179
+ /** terra */
180
+ | "bombay-12"
181
+ | "columbus-5"
182
+ //juno
183
+ | "uni-4"
184
+ | "uni-5";
185
+
186
+ /** string charityId is terra endow address */
187
+ charityId?: string | number;
188
+
189
+ consent_marketing?: boolean;
190
+ consent_tax?: boolean;
191
+ cryptoFee?: number;
192
+ donationFinalChainId?:
193
+ | ChainID.V2DestinationChainID
194
+ | "juno-1"
195
+ | "staging"
196
+ | "uni-4"
197
+ | "uni-5";
198
+
199
+ fiscalSponsorshipFee?: number;
200
+ /** @deprecated */
201
+ fundId?: number;
202
+ /** @deprecated only one record with this value */
203
+ fundMembers?: string[];
204
+ name?: string;
205
+ nftAddress?: string;
206
+ nftRequested?: boolean;
207
+ receiptRequested?: boolean;
208
+
209
+ stateAddress?: string;
210
+ /** initialized as "null" */
211
+ swapDenomination?: string;
212
+ swapFinalAmount?: number;
213
+ swapFinished?: boolean;
214
+ swapStarted?: boolean;
215
+ taxReceiptId?: string;
216
+ tcaAssociation?: string;
217
+ transactionDate: ISODate;
218
+ ustFinal?: number;
219
+ } & PartialV2Kyc &
220
+ // omitted attributes, to not override broader definition
221
+ Partial<
222
+ Omit<
223
+ V2NonKeyAttributes,
224
+ "chainId" | "donationFinalChainId" | "transactionDate"
225
+ >
226
+ >;
227
+
228
+ type DBRecord = PrimaryKey &
229
+ AppUsed_IndexKey &
230
+ Checked_8283_IndexKey &
231
+ EmailIndexKey &
232
+ EndowmentId_IndexKey &
233
+ FiatRamp_IndexKey &
234
+ FundDeposits_IndexKey &
235
+ Network_FinalizedDate_IndexKey &
236
+ Tax_Receipt_Sent_IndexKey &
237
+ WalletAddress_Index &
238
+ NonKeyAttributes;
239
+
240
+ type WithoutKYC = { kycEmail?: never };
241
+ type WithKYC = V2Kyc;
242
+
243
+ /** only use this definition on writes: for reads, it's safer to use DBRecord */
244
+ type V2DBRecord = PrimaryKey &
245
+ EmailIndexKey &
246
+ FiatRamp_IndexKey &
247
+ /** be sure to include in crypto donation */
248
+ WalletAddress_Index &
249
+ Required<EndowmentId_IndexKey> &
250
+ Required<AppUsed_IndexKey> &
251
+ (WithKYC | WithoutKYC) &
252
+ V2NonKeyAttributes;
253
+
254
+ /** only use this definition on writes: for reads, it's safer to use DBRecord */
255
+ type V2InitCryptoDBRecord = PrimaryKey &
256
+ EmailIndexKey &
257
+ /** be sure to include in crypto donation */
258
+ WalletAddress_Index &
259
+ Required<EndowmentId_IndexKey> &
260
+ Required<AppUsed_IndexKey> &
261
+ (WithKYC | WithoutKYC);
262
+
263
+ type TributeNotif = {
264
+ toFullName: string;
265
+ toEmail: string;
266
+ /** may be empty `""` */
267
+ fromMsg: string;
268
+ };
269
+
270
+ export namespace Intent {
271
+ interface Donor {
272
+ /** may be empty `''` */
273
+ title?: DonorTitle;
274
+ firstName: string;
275
+ lastName: string;
276
+ email: string;
277
+ address?: DonorAddress;
278
+ /** signifies eligibility to UK Gift Aid, defaults to `false` for crypto donations */
279
+ ukGiftAid: boolean;
280
+ }
281
+
282
+ interface Attr {
283
+ transactionId?: string;
284
+ amount: number;
285
+ tipAmount: number;
286
+ feeAllowance: number;
287
+ endowmentId: number;
288
+ /** program donation: may be empty `""`*/
289
+ programId?: string;
290
+ programName?: string;
291
+ splitLiq: number;
292
+ donor: Donor;
293
+ /** honorary full name */
294
+ inHonorOf?: string;
295
+ /** only allowed if honoree is provided */
296
+ tributeNotif?: TributeNotif;
297
+ source: Source;
298
+ }
299
+
300
+ interface Crypto extends Attr {
301
+ denomination: string;
302
+ chainId: ChainID.V2CryptoChainID;
303
+ walletAddress?: string;
304
+ chainName: string;
305
+ }
306
+
307
+ interface Fiat extends Attr {
308
+ currency: string;
309
+ }
310
+ }
311
+
312
+ /** web-app determinant: show signup page or not*/
313
+ type GuestDonor = {
314
+ email: string;
315
+ fullName: string;
316
+ };
317
+ }
318
+
319
+ export declare namespace OnHoldDonation {
320
+ type PrimaryKey = {
321
+ transactionId: string; //PK
322
+ };
323
+
324
+ type Index = "Email_Index" | "EndowmentId_Index" | "fiatRamp_Index";
325
+
326
+ type ApesAttributes = {
327
+ client: Extract<Donation.Client, "apes">;
328
+ apesComplianceReviewStatus: Donation.ApesComplianceReviewStatus;
329
+ apesComplianceTimeWindow: ISODate;
330
+ };
331
+
332
+ type CryptoAttributes = {
333
+ chainId: ChainID.V2CryptoChainID;
334
+ destinationChainId: Exclude<ChainID.V2DestinationChainID, "fiat">;
335
+ walletAddress: string;
336
+ /** status of this record should NOT be changed by our pipelines */
337
+ third_party?: true;
338
+ payment_id?: number;
339
+ };
340
+
341
+ type FiatAttributes = {
342
+ chainId: ChainID.V2FiatChainID;
343
+ destinationChainId: Extract<ChainID.V2DestinationChainID, "fiat">;
344
+ /** we can only know the method once it's confirmed by webhook */
345
+ paymentMethod?: string;
346
+ /** only exists if donor opts for manual bank verification */
347
+ stripeDepositVerifyUrl?: string;
348
+ // TODO: create donation intents for subscriptions as well, then uncomment the below line
349
+ // frequency: FiatDonations.Frequency;
350
+ };
351
+
352
+ type NonKeyAttributes = {
353
+ /** tip, fee-allowance is included*/
354
+ amount: number;
355
+ /** tip, fee-allowance is included*/
356
+ usdValue: number;
357
+ /** same denomination as amount */
358
+ tipAmount?: number;
359
+ /** same denomination as amount */
360
+ feeAllowance?: number;
361
+ appUsed: Donation.App;
362
+ chainName: string;
363
+ /** fund name or npo name, as onhold records not yet distributed */
364
+ charityName: string;
365
+ nonProfitMsg?: string;
366
+ /** upper case */
367
+ denomination: string;
368
+ donationFinalized: boolean;
369
+ endowmentId: number;
370
+ /** donation to fund */
371
+ fund_id?: string;
372
+ /** donation to fund */
373
+ fund_name?: string;
374
+ /** endowment ids */
375
+ fund_members?: number[];
376
+ /** program donation: may be empty `""`*/
377
+ programId?: string;
378
+ programName?: string;
379
+ fiscalSponsored: boolean;
380
+ hideBgTip?: boolean;
381
+ network: Environment;
382
+ /** "0" - "100" */
383
+ splitLiq: string;
384
+ status?: "intent" | "pending";
385
+ transactionDate: ISODate;
386
+ /** TTL attribute */
387
+ expireAt?: number;
388
+ /** for new records only: `true` is recurring */
389
+ isRecurring?: boolean;
390
+ /** honorary full name - may be empty `''` */
391
+ inHonorOf?: string;
392
+ /** only allowed if honoree is provided */
393
+ tributeNotif?: Donation.TributeNotif;
394
+ } & (ApesAttributes | EmptyObject) &
395
+ (Donation.WithKYC | Donation.WithoutKYC);
396
+
397
+ type CryptoDBRecord = PrimaryKey &
398
+ NonKeyAttributes &
399
+ CryptoAttributes &
400
+ Donation.EmailIndexKey;
401
+
402
+ type FiatDBRecord = PrimaryKey &
403
+ Required<Donation.FiatRamp_IndexKey> &
404
+ NonKeyAttributes &
405
+ FiatAttributes &
406
+ Required<Donation.EmailIndexKey>; //email is required for fiat donations
407
+
408
+ type DBRecord = CryptoDBRecord | FiatDBRecord;
409
+
410
+ /** Only used when donation is finalized and ready for transfer to Donations DB */
411
+ }
412
+
413
+ export declare namespace FiatDonations {
414
+ type Currency = {
415
+ /** lowercase ISO 4217 code */
416
+ currency_code: string;
417
+ minimum_amount: number;
418
+ rate: number;
419
+ timestamp: string;
420
+ };
421
+
422
+ type Frequency = "one-time" | "subscription";
423
+ }
424
+
425
+ export declare namespace PayPalDonation {
426
+ type CreateOrder = {
427
+ purchase_units: PurchaseUnit[];
428
+ intent: Intent;
429
+ payment_source?: PaymentSource;
430
+ };
431
+
432
+ // see Response object description in official docs
433
+ // https://developer.paypal.com/docs/api/orders/v2/#orders_create
434
+ type Order = {
435
+ id: string;
436
+ status: Status;
437
+ intent: Intent;
438
+ payment_source: PaymentSource;
439
+ purchase_units: PurchaseUnit[];
440
+ payer: Payer;
441
+ create_time: Date;
442
+ links: Link[];
443
+ };
444
+
445
+ type Link = {
446
+ href: string;
447
+ rel: string;
448
+ method: string;
449
+ };
450
+
451
+ type Payer = {
452
+ name: Name;
453
+ email_address: string;
454
+ payer_id: string;
455
+ };
456
+
457
+ type PaymentSource = {
458
+ paypal: {
459
+ name: Name;
460
+ email_address: string;
461
+ account_id: string;
462
+ };
463
+ };
464
+
465
+ type Name = {
466
+ given_name: string;
467
+ surname: string;
468
+ };
469
+
470
+ type PurchaseUnit = {
471
+ // don't need 'reference_id'
472
+ amount: {
473
+ /** Three-character ISO-4217 code. */
474
+ currency_code: string;
475
+ value: string;
476
+ };
477
+ /** @link https://developer.paypal.com/docs/api/orders/v2/#orders_get */
478
+ payments?: {
479
+ captures: CapturedPayment[];
480
+ };
481
+ };
482
+
483
+ type CapturedPayment = {
484
+ seller_receivable_breakdown: {
485
+ gross_amount: { value: number };
486
+ paypal_fee: { value: number };
487
+ net_amount: { value: number };
488
+ };
489
+ };
490
+
491
+ type Intent = "CAPTURE" | "AUTHORIZE";
492
+ type Status =
493
+ | "CREATED"
494
+ | "SAVED"
495
+ | "APPROVED"
496
+ | "VOIDED"
497
+ | "COMPLETED"
498
+ | "PAYER_ACTION_REQUIRED";
499
+ }
500
+
501
+ export declare namespace StripeDonation {
502
+ /** DEPRECATED - can be removed eventually as existing records will eventually "expire" */
503
+ type LegacyMetadata = {
504
+ /** This attr will help in identifying PaymentIntents made for one-time donations only */
505
+ donation_type: Extract<FiatDonations.Frequency, "one-time">;
506
+ /** This attr will link the received webhook event to a donation intent */
507
+ intent_tx_id: string;
508
+ };
509
+
510
+ /**
511
+ * Stripe forces metadata to be typeof `string`
512
+ * @link https://docs.stripe.com/metadata/use-cases#store-structured-data
513
+ */
514
+ type Metadata = {
515
+ [K in keyof OnHoldDonation.FiatDBRecord]: string;
516
+ } & {
517
+ // KYC fields
518
+ title?: Donation.DonorTitle;
519
+ fullName: string;
520
+ ukGiftAid: string;
521
+ streetAddress?: string;
522
+ /** may be empty `''` */
523
+ city?: string;
524
+ /** optional */
525
+ state?: string;
526
+ zipCode?: string;
527
+ country?: string;
528
+ };
529
+
530
+ type SetupIntentMetadata = Metadata & {
531
+ // Subs specific fields
532
+ productId: string;
533
+ subsQuantity: string;
534
+ };
535
+ }
@@ -0,0 +1,48 @@
1
+ import type { Donation } from "./donation.mjs";
2
+
3
+ type FinalRecord = Pick<
4
+ Donation.V2DBRecord,
5
+ | "amount"
6
+ | "appUsed"
7
+ | "chainName"
8
+ | "charityName"
9
+ | "denomination"
10
+ | "email"
11
+ | "endowmentId"
12
+ | "feeAllowance"
13
+ | "fiatRamp"
14
+ | "fiscalSponsored"
15
+ | "inHonorOf"
16
+ | "isRecurring"
17
+ | "network"
18
+ | "nonProfitMsg"
19
+ | "paymentMethod"
20
+ | "programId"
21
+ | "programName"
22
+ | "splitLiq"
23
+ | "tipAmount"
24
+ | "transactionDate"
25
+ | "transactionId"
26
+ | "usdValue"
27
+ >;
28
+
29
+ interface SettledAmounts {
30
+ settledFee: number;
31
+ settledNet: number;
32
+ }
33
+
34
+ /** donation is to fund */
35
+ interface Fund {
36
+ fund_id?: string;
37
+ fund_name?: string;
38
+ fund_members?: number[];
39
+ }
40
+
41
+ export type FinalRecorderPayload = {
42
+ /** intent to delete */
43
+ intentId?: string;
44
+ hideBgTip: boolean;
45
+ } & FinalRecord &
46
+ SettledAmounts &
47
+ Fund &
48
+ (Donation.WithKYC | Donation.WithoutKYC);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @param amount endow amount + tip
3
+ * @param rate 0-1, to be applied to amount @example `amount * rate`
4
+ * @param flat flat amount added to `amount * rate`. NOTE: make sure that this is in the same unit as `amount`
5
+ * @returns number
6
+ */
7
+ export const getMinFeeAllowance = (
8
+ amount: number,
9
+ rate: number,
10
+ flat = 0
11
+ ): number => {
12
+ /**
13
+ * fee(1) = amount * rate + flat
14
+ * fee(2) = (amount + fee(1)) * rate + flat
15
+ * fee(3) = (amount + fee(2)) * rate + flat
16
+ * i.e. F₍ₙ₎ = a · ∑ᵏ⁼¹ⁿ (rᵏ) + f · ∑ᵏ⁼⁰ⁿ⁻¹ (rᵏ)
17
+ * which converges to this formula: n approaches infinity
18
+ */
19
+ return (amount * rate + flat) / (1 - rate);
20
+ };