Skip to content

Commit 9fcf0d7

Browse files
committed
feat: Added getCurrentUserPayments API
1 parent 0b38a71 commit 9fcf0d7

File tree

6 files changed

+310
-11
lines changed

6 files changed

+310
-11
lines changed

firestore-stripe-web-sdk/etc/firestore-stripe-payments.api.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,20 @@ export interface CreateCheckoutSessionOptions {
4444
// @public
4545
export function getCurrentUserPayment(payments: StripePayments, paymentId: string): Promise<Payment>;
4646

47+
// @public (undocumented)
48+
export function getCurrentUserPayments(payments: StripePayments, options?: GetPaymentsOptions): Promise<Payment[]>;
49+
4750
// @public
4851
export function getCurrentUserSubscription(payments: StripePayments, subscriptionId: string): Promise<Subscription>;
4952

5053
// @public
5154
export function getCurrentUserSubscriptions(payments: StripePayments, options?: GetSubscriptionsOptions): Promise<Subscription[]>;
5255

56+
// @public
57+
export interface GetPaymentsOptions {
58+
status?: PaymentStatus | PaymentStatus[];
59+
}
60+
5361
// @public
5462
export function getPrice(payments: StripePayments, productId: string, priceId: string): Promise<Price>;
5563

@@ -126,15 +134,15 @@ export interface Payment {
126134
product: string;
127135
price: string;
128136
}>;
129-
readonly status: PaymentState;
137+
readonly status: PaymentStatus;
130138
readonly uid: string;
131139
}
132140

133141
// @public
134142
export type PaymentMethodType = "card" | "acss_debit" | "afterpay_clearpay" | "alipay" | "bacs_debit" | "bancontact" | "boleto" | "eps" | "fpx" | "giropay" | "grabpay" | "ideal" | "klarna" | "oxxo" | "p24" | "sepa_debit" | "sofort" | "wechat_pay";
135143

136144
// @public
137-
export type PaymentState = "requires_payment_method" | "requires_confirmation" | "requires_action" | "processing" | "requires_capture" | "cancelled" | "succeeded";
145+
export type PaymentStatus = "requires_payment_method" | "requires_confirmation" | "requires_action" | "processing" | "requires_capture" | "cancelled" | "succeeded";
138146

139147
// @public
140148
export interface Price {

firestore-stripe-web-sdk/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ export {
3838
SessionCreateParams,
3939
} from "./session";
4040

41-
export { Payment, PaymentState, getCurrentUserPayment } from "./payment";
41+
export {
42+
GetPaymentsOptions,
43+
Payment,
44+
PaymentStatus,
45+
getCurrentUserPayment,
46+
getCurrentUserPayments,
47+
} from "./payment";
4248

4349
export {
4450
GetProductOptions,

firestore-stripe-web-sdk/src/payment.ts

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,25 @@
1616

1717
import { FirebaseApp } from "@firebase/app";
1818
import {
19+
collection,
1920
doc,
2021
DocumentData,
2122
DocumentReference,
2223
DocumentSnapshot,
2324
Firestore,
2425
FirestoreDataConverter,
2526
getDoc,
27+
getDocs,
2628
getFirestore,
29+
query,
30+
Query,
2731
QueryDocumentSnapshot,
32+
QuerySnapshot,
33+
where,
2834
} from "@firebase/firestore";
2935
import { StripePayments, StripePaymentsError } from "./init";
3036
import { getCurrentUser } from "./user";
31-
import { checkNonEmptyString } from "./utils";
37+
import { checkNonEmptyArray, checkNonEmptyString } from "./utils";
3238

3339
/**
3440
* Interface of a Stripe payment stored in the app database.
@@ -103,7 +109,7 @@ export interface Payment {
103109
/**
104110
* Status of this payment.
105111
*/
106-
readonly status: PaymentState;
112+
readonly status: PaymentStatus;
107113

108114
/**
109115
* Firebase Auth UID of the user that created the payment.
@@ -116,7 +122,7 @@ export interface Payment {
116122
/**
117123
* Possible states a payment can be in.
118124
*/
119-
export type PaymentState =
125+
export type PaymentStatus =
120126
| "requires_payment_method"
121127
| "requires_confirmation"
122128
| "requires_action"
@@ -144,6 +150,43 @@ export function getCurrentUserPayment(
144150
});
145151
}
146152

153+
/**
154+
* Optional parameters for the {@link getCurrentUserPayments} function.
155+
*/
156+
export interface GetPaymentsOptions {
157+
/**
158+
* Specify one or more payment status values to retrieve. When set only the payments
159+
* with the given status are returned.
160+
*/
161+
status?: PaymentStatus | PaymentStatus[];
162+
}
163+
164+
export function getCurrentUserPayments(
165+
payments: StripePayments,
166+
options?: GetPaymentsOptions
167+
): Promise<Payment[]> {
168+
const queryOptions: { status?: PaymentStatus[] } = {};
169+
if (typeof options?.status !== "undefined") {
170+
queryOptions.status = getStatusAsArray(options.status);
171+
}
172+
173+
return getCurrentUser(payments).then((uid: string) => {
174+
const dao: PaymentDAO = getOrInitPaymentDAO(payments);
175+
return dao.getPayments(uid, queryOptions);
176+
});
177+
}
178+
179+
function getStatusAsArray(
180+
status: PaymentStatus | PaymentStatus[]
181+
): PaymentStatus[] {
182+
if (typeof status === "string") {
183+
return [status];
184+
}
185+
186+
checkNonEmptyArray(status, "status must be a non-empty array.");
187+
return status;
188+
}
189+
147190
/**
148191
* Internal interface for all database interactions pertaining to Stripe payments. Exported
149192
* for testing.
@@ -152,6 +195,10 @@ export function getCurrentUserPayment(
152195
*/
153196
export interface PaymentDAO {
154197
getPayment(uid: string, paymentId: string): Promise<Payment>;
198+
getPayments(
199+
uid: string,
200+
options?: { status?: PaymentStatus[] }
201+
): Promise<Payment[]>;
155202
}
156203

157204
const PAYMENT_CONVERTER: FirestoreDataConverter<Payment> = {
@@ -209,6 +256,22 @@ class FirestorePaymentDAO implements PaymentDAO {
209256
return snap.data();
210257
}
211258

259+
public async getPayments(
260+
uid: string,
261+
options?: { status?: PaymentStatus[] }
262+
): Promise<Payment[]> {
263+
const querySnap: QuerySnapshot<Payment> = await this.getPaymentSnapshots(
264+
uid,
265+
options?.status
266+
);
267+
const payments: Payment[] = [];
268+
querySnap.forEach((snap: QueryDocumentSnapshot<Payment>) => {
269+
payments.push(snap.data());
270+
});
271+
272+
return payments;
273+
}
274+
212275
private async getPaymentSnapshotIfExists(
213276
uid: string,
214277
paymentId: string
@@ -233,6 +296,23 @@ class FirestorePaymentDAO implements PaymentDAO {
233296
return snapshot;
234297
}
235298

299+
private async getPaymentSnapshots(
300+
uid: string,
301+
status?: PaymentStatus[]
302+
): Promise<QuerySnapshot<Payment>> {
303+
let paymentsQuery: Query<Payment> = collection(
304+
this.firestore,
305+
this.customersCollection,
306+
uid,
307+
PAYMENTS_COLLECTION
308+
).withConverter(PAYMENT_CONVERTER);
309+
if (status) {
310+
paymentsQuery = query(paymentsQuery, where("status", "in", status));
311+
}
312+
313+
return await this.queryFirestore(() => getDocs(paymentsQuery));
314+
}
315+
236316
private async queryFirestore<T>(fn: () => Promise<T>): Promise<T> {
237317
try {
238318
return await fn();

firestore-stripe-web-sdk/test/emulator.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
import {
4141
createCheckoutSession,
4242
getCurrentUserPayment,
43+
getCurrentUserPayments,
4344
getCurrentUserSubscription,
4445
getCurrentUserSubscriptions,
4546
getPrice,
@@ -61,6 +62,7 @@ import {
6162
economyPlan,
6263
payment1,
6364
payment2,
65+
payment3,
6466
PaymentData,
6567
premiumPlan,
6668
premiumPlanPrice,
@@ -1005,6 +1007,68 @@ describe("Emulator tests", () => {
10051007
});
10061008
});
10071009

1010+
describe("getCurrentUserPayments()", () => {
1011+
context("without user signed in", () => {
1012+
it("rejects when fetching payments", async () => {
1013+
const err: any = await expect(
1014+
getCurrentUserPayments(payments)
1015+
).to.be.rejectedWith(
1016+
"Failed to determine currently signed in user. User not signed in."
1017+
);
1018+
1019+
expect(err).to.be.instanceOf(StripePaymentsError);
1020+
expect(err.code).to.equal("unauthenticated");
1021+
expect(err.cause).to.be.undefined;
1022+
});
1023+
});
1024+
1025+
context("with user signed in", () => {
1026+
let currentUser: string = "";
1027+
1028+
before(async () => {
1029+
currentUser = (await signInAnonymously(auth)).user.uid;
1030+
await addUserData(currentUser);
1031+
await addPaymentData(currentUser, rawPaymentData);
1032+
});
1033+
1034+
after(async () => {
1035+
await signOut(auth);
1036+
});
1037+
1038+
it("should return all payments when called without options", async () => {
1039+
const paymentData: Payment[] = await getCurrentUserPayments(payments);
1040+
1041+
const expected: Payment[] = [
1042+
{ ...payment1, uid: currentUser },
1043+
{ ...payment2, uid: currentUser },
1044+
{ ...payment3, uid: currentUser },
1045+
];
1046+
expect(paymentData).to.eql(expected);
1047+
});
1048+
1049+
it("should only return payments with the given status", async () => {
1050+
const paymentData: Payment[] = await getCurrentUserPayments(payments, {
1051+
status: "succeeded",
1052+
});
1053+
1054+
const expected: Payment[] = [{ ...payment1, uid: currentUser }];
1055+
expect(paymentData).to.eql(expected);
1056+
});
1057+
1058+
it("should only return payments with the given statuses", async () => {
1059+
const paymentData: Payment[] = await getCurrentUserPayments(payments, {
1060+
status: ["succeeded", "requires_action"],
1061+
});
1062+
1063+
const expected: Payment[] = [
1064+
{ ...payment1, uid: currentUser },
1065+
{ ...payment2, uid: currentUser },
1066+
];
1067+
expect(paymentData).to.eql(expected);
1068+
});
1069+
});
1070+
});
1071+
10081072
async function addProductData(
10091073
productId: string,
10101074
data: ProductData

0 commit comments

Comments
 (0)