@better-giving/donation 3.0.4 → 3.0.5

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.
@@ -0,0 +1,73 @@
1
+ export declare namespace FiatDonations {
2
+ type Currency = {
3
+ /** lowercase ISO 4217 code */
4
+ currency_code: string;
5
+ minimum_amount: number;
6
+ rate: number;
7
+ timestamp: string;
8
+ };
9
+ }
10
+ export declare namespace PayPalDonation {
11
+ type CreateOrder = {
12
+ purchase_units: PurchaseUnit[];
13
+ intent: Intent;
14
+ payment_source?: PaymentSource;
15
+ };
16
+ type Order = {
17
+ id: string;
18
+ status: Status;
19
+ intent: Intent;
20
+ payment_source: PaymentSource;
21
+ purchase_units: PurchaseUnit[];
22
+ payer: Payer;
23
+ create_time: Date;
24
+ links: Link[];
25
+ };
26
+ type Link = {
27
+ href: string;
28
+ rel: string;
29
+ method: string;
30
+ };
31
+ type Payer = {
32
+ name: Name;
33
+ email_address: string;
34
+ payer_id: string;
35
+ };
36
+ type PaymentSource = {
37
+ paypal: {
38
+ name: Name;
39
+ email_address: string;
40
+ account_id: string;
41
+ };
42
+ };
43
+ type Name = {
44
+ given_name: string;
45
+ surname: string;
46
+ };
47
+ type PurchaseUnit = {
48
+ amount: {
49
+ /** Three-character ISO-4217 code. */
50
+ currency_code: string;
51
+ value: string;
52
+ };
53
+ /** @link https://developer.paypal.com/docs/api/orders/v2/#orders_get */
54
+ payments?: {
55
+ captures: CapturedPayment[];
56
+ };
57
+ };
58
+ type CapturedPayment = {
59
+ seller_receivable_breakdown: {
60
+ gross_amount: {
61
+ value: number;
62
+ };
63
+ paypal_fee: {
64
+ value: number;
65
+ };
66
+ net_amount: {
67
+ value: number;
68
+ };
69
+ };
70
+ };
71
+ type Intent = "CAPTURE" | "AUTHORIZE";
72
+ type Status = "CREATED" | "SAVED" | "APPROVED" | "VOIDED" | "COMPLETED" | "PAYER_ACTION_REQUIRED";
73
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/schema.d.mts CHANGED
@@ -1,13 +1,13 @@
1
1
  import * as v from "valibot";
2
2
  export declare const frequencies: readonly ["one-time", "recurring"];
3
3
  export declare const frequency: v.PicklistSchema<readonly ["one-time", "recurring"], "Please select donation frequency">;
4
- export type Frequency = v.InferOutput<typeof frequency>;
4
+ export type TFrequency = v.InferOutput<typeof frequency>;
5
5
  export declare const donation_sources: readonly ["bg-marketplace", "bg-widget", "tester-app"];
6
6
  export declare const donation_source: v.PicklistSchema<readonly ["bg-marketplace", "bg-widget", "tester-app"], undefined>;
7
- export type DonationSource = v.InferOutput<typeof donation_source>;
7
+ export type TDonationSource = v.InferOutput<typeof donation_source>;
8
8
  export declare const donor_titles: readonly ["Mr", "Mrs", "Ms", "Mx", ""];
9
9
  export declare const donor_title: v.PicklistSchema<readonly ["Mr", "Mrs", "Ms", "Mx", ""], undefined>;
10
- export type DonorTitle = v.InferOutput<typeof donor_title>;
10
+ export type TDonorTitle = v.InferOutput<typeof donor_title>;
11
11
  export declare const allocation: v.SchemaWithPipe<[v.ObjectSchema<{
12
12
  readonly liq: v.SchemaWithPipe<[v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 100, undefined>]>;
13
13
  readonly lock: v.SchemaWithPipe<[v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 100, undefined>]>;
@@ -17,4 +17,38 @@ export declare const allocation: v.SchemaWithPipe<[v.ObjectSchema<{
17
17
  lock: number;
18
18
  cash: number;
19
19
  }, "Allocation must sum to 100%">]>;
20
- export type Allocation = v.InferOutput<typeof allocation>;
20
+ export type IAllocation = v.InferOutput<typeof allocation>;
21
+ export declare const page_opts: v.ObjectSchema<{
22
+ readonly limit: v.OptionalSchema<v.SchemaWithPipe<[v.SchemaWithPipe<[v.StringSchema<"required">, v.TrimAction]>, v.TransformAction<string, number>, v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>]>, never>;
23
+ readonly next: v.OptionalSchema<v.SchemaWithPipe<[v.StringSchema<undefined>, v.Base64Action<string, undefined>]>, never>;
24
+ }, undefined>;
25
+ export interface IPageOpts extends v.InferOutput<typeof page_opts> {
26
+ }
27
+ declare const donations_search_raw: v.ObjectSchema<{
28
+ readonly date_start: v.OptionalSchema<v.LazySchema<v.StringSchema<undefined> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.NonEmptyAction<string, "required">]> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.TransformAction<string, Date>, v.DateSchema<"invalid date">, v.MaxValueAction<Date, Date, "can't be later than today">, v.TransformAction<Date, string>]>>, never>;
29
+ readonly date_end: v.OptionalSchema<v.LazySchema<v.StringSchema<undefined> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.NonEmptyAction<string, "required">]> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.TransformAction<string, Date>, v.DateSchema<"invalid date">, v.MaxValueAction<Date, Date, "can't be later than today">, v.TransformAction<Date, string>]>>, never>;
30
+ readonly limit: v.OptionalSchema<v.SchemaWithPipe<[v.SchemaWithPipe<[v.StringSchema<"required">, v.TrimAction]>, v.TransformAction<string, number>, v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>]>, never>;
31
+ readonly next: v.OptionalSchema<v.SchemaWithPipe<[v.StringSchema<undefined>, v.Base64Action<string, undefined>]>, never>;
32
+ }, undefined>;
33
+ export declare const donations_search: v.SchemaWithPipe<[v.ObjectSchema<{
34
+ readonly date_start: v.OptionalSchema<v.LazySchema<v.StringSchema<undefined> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.NonEmptyAction<string, "required">]> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.TransformAction<string, Date>, v.DateSchema<"invalid date">, v.MaxValueAction<Date, Date, "can't be later than today">, v.TransformAction<Date, string>]>>, never>;
35
+ readonly date_end: v.OptionalSchema<v.LazySchema<v.StringSchema<undefined> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.NonEmptyAction<string, "required">]> | v.SchemaWithPipe<[v.StringSchema<undefined>, v.TransformAction<string, Date>, v.DateSchema<"invalid date">, v.MaxValueAction<Date, Date, "can't be later than today">, v.TransformAction<Date, string>]>>, never>;
36
+ readonly limit: v.OptionalSchema<v.SchemaWithPipe<[v.SchemaWithPipe<[v.StringSchema<"required">, v.TrimAction]>, v.TransformAction<string, number>, v.SchemaWithPipe<[v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>]>, never>;
37
+ readonly next: v.OptionalSchema<v.SchemaWithPipe<[v.StringSchema<undefined>, v.Base64Action<string, undefined>]>, never>;
38
+ }, undefined>, v.BaseValidation<{
39
+ limit?: number | undefined;
40
+ next?: string | undefined;
41
+ date_start?: string | undefined;
42
+ date_end?: string | undefined;
43
+ }, {
44
+ limit?: number | undefined;
45
+ next?: string | undefined;
46
+ date_start?: string | undefined;
47
+ date_end?: string | undefined;
48
+ }, v.PartialCheckIssue<{
49
+ date_start?: string | undefined;
50
+ date_end?: string | undefined;
51
+ }>>]>;
52
+ export interface IDonationsSearch extends v.InferOutput<typeof donations_search_raw> {
53
+ }
54
+ export {};
package/dist/schema.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { $int_gte1 } from "@better-giving/schemas";
2
+ import { iso_date, startOfDay, endOfDay } from "@better-giving/schemas/date";
1
3
  import * as v from "valibot";
2
4
  export const frequencies = ["one-time", "recurring"];
3
5
  export const frequency = v.picklist(frequencies, "Please select donation frequency");
@@ -15,3 +17,15 @@ export const allocation = v.pipe(v.object({
15
17
  lock: pct,
16
18
  cash: pct,
17
19
  }), v.check((x) => x.liq + x.lock + x.cash === 100, "Allocation must sum to 100%"));
20
+ export const page_opts = v.object({
21
+ limit: v.optional($int_gte1),
22
+ next: v.optional(v.pipe(v.string(), v.base64())),
23
+ });
24
+ const donations_search_raw = v.object({
25
+ ...page_opts.entries,
26
+ date_start: v.optional(iso_date(startOfDay)),
27
+ date_end: v.optional(iso_date(endOfDay)),
28
+ });
29
+ export const donations_search = v.pipe(donations_search_raw, v.forward(v.partialCheck([["date_start"], ["date_end"]], ({ date_start: a, date_end: b }) => {
30
+ return a && b ? a <= b : true;
31
+ }, "start date must be earlier than end date"), ["date_start"]));
@@ -0,0 +1,11 @@
1
+ import type { ISubscription } from "./interfaces.mjs";
2
+ import { Db } from "@better-giving/db";
3
+ export declare class SubsDb extends Db {
4
+ static readonly table = "subscriptions";
5
+ key(id: string): {
6
+ subscription_id: string;
7
+ };
8
+ put(data: ISubscription): Promise<import("@aws-sdk/lib-dynamodb").PutCommandOutput>;
9
+ update(id: string, data: Partial<Omit<ISubscription, "subscription_id">>): Promise<ISubscription>;
10
+ del(id: string): Promise<import("@aws-sdk/lib-dynamodb").DeleteCommandOutput>;
11
+ }
@@ -0,0 +1,35 @@
1
+ import { DeleteCommand, PutCommand, UpdateCommand, } from "@aws-sdk/lib-dynamodb";
2
+ import { Db, UpdateBuilder } from "@better-giving/db";
3
+ export class SubsDb extends Db {
4
+ static table = "subscriptions";
5
+ key(id) {
6
+ return { subscription_id: id };
7
+ }
8
+ async put(data) {
9
+ const cmd = new PutCommand({
10
+ TableName: SubsDb.table,
11
+ Item: data,
12
+ ConditionExpression: `attribute_not_exists(${"subscription_id"})`,
13
+ });
14
+ return this.client.send(cmd);
15
+ }
16
+ async update(id, data) {
17
+ const upd8 = new UpdateBuilder();
18
+ for (const k in data) {
19
+ upd8.set(k, data[k]);
20
+ }
21
+ const cmd = new UpdateCommand({
22
+ TableName: SubsDb.table,
23
+ Key: this.key(id),
24
+ ...upd8.collect(),
25
+ });
26
+ return this.client.send(cmd).then((r) => r.Attributes);
27
+ }
28
+ async del(id) {
29
+ const cmd = new DeleteCommand({
30
+ TableName: SubsDb.table,
31
+ Key: this.key(id),
32
+ });
33
+ return this.client.send(cmd);
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "name": "@better-giving/donation",
3
- "version": "3.0.4",
3
+ "version": "3.0.5",
4
4
  "devDependencies": {
5
5
  "@better-giving/config": "1.1.2"
6
6
  },
7
7
  "peerDependencies": {
8
8
  "@better-giving/types": "1.1.8",
9
+ "@better-giving/db": "2.0.6",
9
10
  "@better-giving/schemas": "2.0.3",
10
- "valibot": "0.42.0"
11
+ "@aws-sdk/lib-dynamodb": "3.485.0",
12
+ "valibot": "0.42.0",
13
+ "date-fns": "4.1.0"
11
14
  },
12
15
  "files": [
13
16
  "src",
14
17
  "dist"
15
18
  ],
16
19
  "exports": {
17
- ".": "./dist/donation.mjs",
20
+ ".": "./dist/index.mjs",
18
21
  "./*": "./dist/*.mjs"
19
22
  },
20
23
  "scripts": {
@@ -0,0 +1,73 @@
1
+ import { Db } from "@better-giving/db";
2
+ import type { ISODate } from "@better-giving/types/alias";
3
+ import type { Environment } from "@better-giving/types/list";
4
+ import type { IPageOpts } from "./schema.mjs";
5
+ import { QueryCommand } from "@aws-sdk/lib-dynamodb";
6
+ import type { IPageKeyed } from "@better-giving/types/api";
7
+ import type { IPublicDonor } from "./interfaces.mjs";
8
+
9
+ export type DMKey = `DM#${string}`;
10
+
11
+ export class DonationDonorsDb extends Db {
12
+ static readonly table = "donation_messages";
13
+ static readonly gsi1 = "gsi1";
14
+ key(id: string) {
15
+ return { PK: `DM#${id}`, SK: `DM#${id}` } as const;
16
+ }
17
+ gsi1_by_recipient(recipient: string, date: string) {
18
+ return {
19
+ gsi1PK: `Recipient#${recipient}#${this.env}`,
20
+ gsi1SK: date as ISODate,
21
+ } as const;
22
+ }
23
+
24
+ list(recipient: string, opts?: IPageOpts): Promise<IPageKeyed<IPublicDonor>> {
25
+ const cmd = new QueryCommand({
26
+ TableName: DonationDonorsDb.table,
27
+ IndexName: DonationDonorsDb.gsi1,
28
+ KeyConditionExpression: "gsi1PK = :gsi1PK",
29
+ ExpressionAttributeValues: {
30
+ ":gsi1PK": this.gsi1_by_recipient(recipient, "only-sk-is-used").gsi1PK,
31
+ },
32
+ Limit: opts?.limit,
33
+ ExclusiveStartKey: this.key_to_obj(opts?.next),
34
+ ScanIndexForward: false,
35
+ });
36
+ return this.client.send(cmd).then(this.to_page<IPublicDonor>);
37
+ }
38
+ }
39
+
40
+ export declare namespace GSI1 {
41
+ type Name = "recipient-env-gsi";
42
+
43
+ interface Key {
44
+ /** `Recipient#{recipient_id}#{env}` */
45
+ gsi1PK: `Recipient#${string}#${Environment}`;
46
+ gsi1SK: ISODate;
47
+ }
48
+ }
49
+
50
+ export declare namespace DonationMessage {
51
+ /** `DM#{uuid}` */
52
+ interface PrimaryKey {
53
+ PK: DMKey;
54
+ SK: DMKey;
55
+ }
56
+
57
+ interface NonKeyAttributes {
58
+ /** USD amount at the time of donation */
59
+ amount: number;
60
+ date: ISODate;
61
+ donation_id: string;
62
+ donor_id: string;
63
+ donor_message: string;
64
+ donor_name: string;
65
+ env: Environment;
66
+ /** uuidv4 */
67
+ id: string;
68
+ /** Endow or fund ID */
69
+ recipient_id: string;
70
+ }
71
+
72
+ interface DBRecord extends PrimaryKey, NonKeyAttributes, GSI1.Key {}
73
+ }
@@ -0,0 +1,133 @@
1
+ import { Db } from "@better-giving/db";
2
+ import type {
3
+ IDonationFinal,
4
+ IDonationFinalAttr,
5
+ TExplicit,
6
+ } from "./interfaces.mjs";
7
+ import {
8
+ PutCommand,
9
+ QueryCommand,
10
+ type QueryCommandInput,
11
+ } from "@aws-sdk/lib-dynamodb";
12
+ import type { IPageKeyed } from "@better-giving/types/api";
13
+ import type { IDonationsSearch } from "./schema.mjs";
14
+
15
+ type K = keyof IDonationFinalAttr;
16
+
17
+ export class DonationsDb extends Db {
18
+ static readonly table = "Donations";
19
+ static readonly gsi_referrer$settled_date = "Referrer-FinalizedDate_index";
20
+ static readonly gsi_npo$settled_date = "npo-settled_date-gsi";
21
+ static readonly gsi_email$tx_date = "email-tx_date-gsii";
22
+
23
+ async put_txi(data: TExplicit<IDonationFinalAttr>) {
24
+ const cmd = new PutCommand({
25
+ TableName: DonationsDb.table,
26
+ Item: data,
27
+ ConditionExpression: `attribute_not_exists(${"transactionId" satisfies keyof IDonationFinalAttr})`,
28
+ });
29
+ return this.client.send(cmd);
30
+ }
31
+
32
+ async referred_by_q(referrer: string) {
33
+ const q: QueryCommandInput = {
34
+ TableName: DonationsDb.table,
35
+ IndexName: DonationsDb.gsi_referrer$settled_date,
36
+ KeyConditionExpression: "#referrer = :referrer",
37
+ ExpressionAttributeNames: {
38
+ "#referrer": "referrer" satisfies K,
39
+ },
40
+ ExpressionAttributeValues: {
41
+ ":referrer": referrer,
42
+ },
43
+ };
44
+ return q;
45
+ }
46
+
47
+ async list_to_npo(
48
+ npo: number,
49
+ opts?: IDonationsSearch & { date_start?: string; date_end?: string }
50
+ ): Promise<IPageKeyed<IDonationFinal>> {
51
+ /** key condition expression */
52
+ let kce = "#npo = :npo";
53
+ /** expression attribute names */
54
+ const ean: Record<string, string> = {
55
+ "#npo": "endowmentId" satisfies K,
56
+ };
57
+ /** expression attribute values */
58
+ const eav: Record<string, any> = {
59
+ ":npo": npo,
60
+ };
61
+
62
+ if (opts?.date_start && opts?.date_end) {
63
+ kce += " AND #settled_date BETWEEN :date_start AND :date_end";
64
+ ean["#settled_date"] = "donationFinalTxDate" satisfies K;
65
+ eav[":date_start"] = opts.date_start;
66
+ eav[":date_end"] = opts.date_end;
67
+ } else if (opts?.date_start) {
68
+ kce += " AND #settled_date >= :date_start";
69
+ ean["#settled_date"] = "donationFinalTxDate" satisfies K;
70
+ eav[":date_start"] = opts.date_start;
71
+ } else if (opts?.date_end) {
72
+ kce += " AND #settled_date <= :date_end";
73
+ ean["#settled_date"] = "donationFinalTxDate" satisfies K;
74
+ eav[":date_end"] = opts.date_end;
75
+ }
76
+
77
+ const cmd = new QueryCommand({
78
+ TableName: DonationsDb.table,
79
+ IndexName: DonationsDb.gsi_npo$settled_date,
80
+ KeyConditionExpression: kce,
81
+ ExpressionAttributeNames: ean,
82
+ ExpressionAttributeValues: eav,
83
+ Limit: opts?.limit,
84
+ ExclusiveStartKey: this.key_to_obj(opts?.next),
85
+ ScanIndexForward: false,
86
+ });
87
+ return this.client.send(cmd).then(this.to_page<IDonationFinal>);
88
+ }
89
+
90
+ async list_by_email(
91
+ email: string,
92
+ opts?: IDonationsSearch
93
+ ): Promise<IPageKeyed<IDonationFinal>> {
94
+ /** key condition expression */
95
+ let kce = "#email = :email";
96
+ /** expression attribute names */
97
+ const ean: Record<string, string> = {
98
+ "#email": "email" satisfies K,
99
+ };
100
+ /** expression attribute values */
101
+ const eav: Record<string, any> = {
102
+ ":email": email,
103
+ };
104
+
105
+ if (opts?.date_start && opts?.date_end) {
106
+ kce += " AND #settled_date BETWEEN :date_start AND :date_end";
107
+ ean["#settled_date"] = "transactionDate" satisfies K;
108
+ eav[":date_start"] = opts.date_start;
109
+ eav[":date_end"] = opts.date_end;
110
+ } else if (opts?.date_start) {
111
+ kce += " AND #settled_date >= :date_start";
112
+ ean["#settled_date"] = "transactionDate" satisfies K;
113
+ eav[":date_start"] = opts.date_start;
114
+ } else if (opts?.date_end) {
115
+ kce += " AND #settled_date <= :date_end";
116
+ ean["#settled_date"] = "transactionDate" satisfies K;
117
+ eav[":date_end"] = opts.date_end;
118
+ }
119
+
120
+ const cmd = new QueryCommand({
121
+ TableName: DonationsDb.table,
122
+ IndexName: DonationsDb.gsi_email$tx_date,
123
+ KeyConditionExpression: kce,
124
+ ExpressionAttributeNames: ean,
125
+ ExpressionAttributeValues: eav,
126
+ Limit: opts?.limit,
127
+ ExclusiveStartKey: this.key_to_obj(opts?.next),
128
+ ScanIndexForward: false,
129
+ });
130
+
131
+ return this.client.send(cmd).then(this.to_page<IDonationFinal>);
132
+ }
133
+ }
package/src/index.mts ADDED
@@ -0,0 +1,14 @@
1
+ export type {
2
+ IAllocation,
3
+ TDonationSource,
4
+ TDonorTitle,
5
+ TFrequency,
6
+ IDonationsSearch,
7
+ IPageOpts,
8
+ } from "./schema.mjs";
9
+
10
+ export type * from "./paypal.mjs";
11
+ export type * from "./interfaces.mjs";
12
+
13
+ export { DonationsDb } from "./donations-db.mjs";
14
+ export { OnHoldDonationsDb } from "./onhold-db.mjs";