@bash-app/bash-common 30.262.0 → 30.265.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/dist/__tests__/requestAuthUser.test.d.ts +2 -0
- package/dist/__tests__/requestAuthUser.test.d.ts.map +1 -0
- package/dist/__tests__/requestAuthUser.test.js +84 -0
- package/dist/__tests__/requestAuthUser.test.js.map +1 -0
- package/dist/__tests__/serviceCheckoutPaymentMethods.test.d.ts +2 -0
- package/dist/__tests__/serviceCheckoutPaymentMethods.test.d.ts.map +1 -0
- package/dist/__tests__/serviceCheckoutPaymentMethods.test.js +41 -0
- package/dist/__tests__/serviceCheckoutPaymentMethods.test.js.map +1 -0
- package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.d.ts +2 -0
- package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.d.ts.map +1 -0
- package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.js +30 -0
- package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.js.map +1 -0
- package/dist/definitions.d.ts +2 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +1 -0
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +3 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/requestAuthUser.d.ts +36 -0
- package/dist/requestAuthUser.d.ts.map +1 -0
- package/dist/requestAuthUser.js +74 -0
- package/dist/requestAuthUser.js.map +1 -0
- package/dist/ticketBnplPaymentMethods.d.ts +30 -1
- package/dist/ticketBnplPaymentMethods.d.ts.map +1 -1
- package/dist/ticketBnplPaymentMethods.js +41 -0
- package/dist/ticketBnplPaymentMethods.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +22 -8
- package/src/__tests__/requestAuthUser.test.ts +120 -0
- package/src/__tests__/serviceCheckoutPaymentMethods.test.ts +59 -0
- package/src/__tests__/ticketBnplPaymentMethods.bankTransfer.test.ts +42 -0
- package/src/definitions.ts +3 -0
- package/src/extendedSchemas.ts +3 -0
- package/src/index.ts +1 -0
- package/src/requestAuthUser.ts +105 -0
- package/src/ticketBnplPaymentMethods.ts +99 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { UserRole } from "@prisma/client";
|
|
2
|
+
import {
|
|
3
|
+
parseRequestAuthFromJwtPayload,
|
|
4
|
+
parseUserRolesFromJwtClaim,
|
|
5
|
+
userHasSuperUserAccess,
|
|
6
|
+
userHasAdminAccess,
|
|
7
|
+
} from "../requestAuthUser.js";
|
|
8
|
+
|
|
9
|
+
describe("parseUserRolesFromJwtClaim", () => {
|
|
10
|
+
it("returns undefined when claim is absent", () => {
|
|
11
|
+
expect(parseUserRolesFromJwtClaim(undefined)).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("defaults empty array to User", () => {
|
|
15
|
+
expect(parseUserRolesFromJwtClaim([])).toEqual([UserRole.User]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("accepts valid enum values", () => {
|
|
19
|
+
expect(parseUserRolesFromJwtClaim([UserRole.User, UserRole.SuperUser])).toEqual([
|
|
20
|
+
UserRole.User,
|
|
21
|
+
UserRole.SuperUser,
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects non-array and unknown role strings", () => {
|
|
26
|
+
expect(parseUserRolesFromJwtClaim("User")).toBeNull();
|
|
27
|
+
expect(parseUserRolesFromJwtClaim(["NotARealRole"])).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("parseRequestAuthFromJwtPayload", () => {
|
|
32
|
+
it("parses session-style payload with roles", () => {
|
|
33
|
+
expect(
|
|
34
|
+
parseRequestAuthFromJwtPayload({
|
|
35
|
+
id: "u-1",
|
|
36
|
+
email: "a@test.com",
|
|
37
|
+
roles: [UserRole.SuperUser],
|
|
38
|
+
isSuperUser: false,
|
|
39
|
+
givenName: "Ada",
|
|
40
|
+
}),
|
|
41
|
+
).toEqual({
|
|
42
|
+
id: "u-1",
|
|
43
|
+
email: "a@test.com",
|
|
44
|
+
roles: [UserRole.SuperUser],
|
|
45
|
+
isSuperUser: undefined,
|
|
46
|
+
username: undefined,
|
|
47
|
+
givenName: "Ada",
|
|
48
|
+
familyName: undefined,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns null when roles claim is absent", () => {
|
|
53
|
+
expect(
|
|
54
|
+
parseRequestAuthFromJwtPayload({
|
|
55
|
+
id: "u-legacy",
|
|
56
|
+
email: "legacy@test.com",
|
|
57
|
+
}),
|
|
58
|
+
).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns null when identity claims are missing or roles are invalid", () => {
|
|
62
|
+
expect(parseRequestAuthFromJwtPayload(null)).toBeNull();
|
|
63
|
+
expect(parseRequestAuthFromJwtPayload({ email: "a@test.com" })).toBeNull();
|
|
64
|
+
expect(
|
|
65
|
+
parseRequestAuthFromJwtPayload({
|
|
66
|
+
id: "u-1",
|
|
67
|
+
email: "a@test.com",
|
|
68
|
+
roles: ["BadRole"],
|
|
69
|
+
}),
|
|
70
|
+
).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("userHasSuperUserAccess", () => {
|
|
75
|
+
it("allows SuperUser role or isSuperUser flag", () => {
|
|
76
|
+
expect(
|
|
77
|
+
userHasSuperUserAccess({
|
|
78
|
+
roles: [UserRole.User, UserRole.SuperUser],
|
|
79
|
+
}),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
expect(
|
|
82
|
+
userHasSuperUserAccess({
|
|
83
|
+
roles: [UserRole.User],
|
|
84
|
+
isSuperUser: true,
|
|
85
|
+
}),
|
|
86
|
+
).toBe(true);
|
|
87
|
+
expect(
|
|
88
|
+
userHasSuperUserAccess({
|
|
89
|
+
roles: [UserRole.User],
|
|
90
|
+
isSuperUser: false,
|
|
91
|
+
}),
|
|
92
|
+
).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("userHasAdminAccess", () => {
|
|
97
|
+
it("allows Admin role", () => {
|
|
98
|
+
expect(
|
|
99
|
+
userHasAdminAccess({ roles: [UserRole.Admin] }),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("allows SuperUser role", () => {
|
|
104
|
+
expect(
|
|
105
|
+
userHasAdminAccess({ roles: [UserRole.SuperUser] }),
|
|
106
|
+
).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("allows isSuperUser flag regardless of role", () => {
|
|
110
|
+
expect(
|
|
111
|
+
userHasAdminAccess({ roles: [UserRole.User], isSuperUser: true }),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rejects plain User role", () => {
|
|
116
|
+
expect(
|
|
117
|
+
userHasAdminAccess({ roles: [UserRole.User], isSuperUser: false }),
|
|
118
|
+
).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveServiceCheckoutPaymentMethodTypes,
|
|
5
|
+
serviceCheckoutOffersAchDebit,
|
|
6
|
+
serviceCheckoutOffersBankTransfer,
|
|
7
|
+
} from "../ticketBnplPaymentMethods.js";
|
|
8
|
+
|
|
9
|
+
describe("resolveServiceCheckoutPaymentMethodTypes", () => {
|
|
10
|
+
it("includes customer_balance when bank transfer enabled", () => {
|
|
11
|
+
expect(
|
|
12
|
+
resolveServiceCheckoutPaymentMethodTypes({
|
|
13
|
+
bankTransferEnabled: true,
|
|
14
|
+
totalChargedCents: 250_000,
|
|
15
|
+
currency: "usd",
|
|
16
|
+
})
|
|
17
|
+
).toContain("customer_balance");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("includes us_bank_account when ACH enabled", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveServiceCheckoutPaymentMethodTypes({
|
|
23
|
+
achDebitEnabled: true,
|
|
24
|
+
totalChargedCents: 250_000,
|
|
25
|
+
currency: "usd",
|
|
26
|
+
})
|
|
27
|
+
).toContain("us_bank_account");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("excludes async methods when globally disabled", () => {
|
|
31
|
+
expect(
|
|
32
|
+
resolveServiceCheckoutPaymentMethodTypes({
|
|
33
|
+
bankTransferEnabled: true,
|
|
34
|
+
achDebitEnabled: true,
|
|
35
|
+
totalChargedCents: 250_000,
|
|
36
|
+
currency: "usd",
|
|
37
|
+
bankTransferGloballyEnabled: false,
|
|
38
|
+
achDebitGloballyEnabled: false,
|
|
39
|
+
})
|
|
40
|
+
).toEqual(["card", "link"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("serviceCheckoutOffers* reflects resolver", () => {
|
|
44
|
+
expect(
|
|
45
|
+
serviceCheckoutOffersBankTransfer({
|
|
46
|
+
bankTransferEnabled: true,
|
|
47
|
+
totalChargedCents: 10_000,
|
|
48
|
+
currency: "usd",
|
|
49
|
+
})
|
|
50
|
+
).toBe(true);
|
|
51
|
+
expect(
|
|
52
|
+
serviceCheckoutOffersAchDebit({
|
|
53
|
+
achDebitEnabled: true,
|
|
54
|
+
totalChargedCents: 10_000,
|
|
55
|
+
currency: "usd",
|
|
56
|
+
})
|
|
57
|
+
).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveTicketCheckoutPaymentMethodTypes,
|
|
5
|
+
ticketCheckoutOffersBankTransfer,
|
|
6
|
+
} from "../ticketBnplPaymentMethods.js";
|
|
7
|
+
|
|
8
|
+
describe("resolveTicketCheckoutPaymentMethodTypes bank transfer", () => {
|
|
9
|
+
it("includes customer_balance when host toggle and USD order", () => {
|
|
10
|
+
expect(
|
|
11
|
+
resolveTicketCheckoutPaymentMethodTypes({
|
|
12
|
+
installmentsEnabled: false,
|
|
13
|
+
bankTransferEnabled: true,
|
|
14
|
+
totalChargedCents: 50_000,
|
|
15
|
+
currency: "usd",
|
|
16
|
+
})
|
|
17
|
+
).toContain("customer_balance");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("excludes customer_balance when globally disabled", () => {
|
|
21
|
+
expect(
|
|
22
|
+
resolveTicketCheckoutPaymentMethodTypes({
|
|
23
|
+
installmentsEnabled: false,
|
|
24
|
+
bankTransferEnabled: true,
|
|
25
|
+
totalChargedCents: 50_000,
|
|
26
|
+
currency: "usd",
|
|
27
|
+
bankTransferGloballyEnabled: false,
|
|
28
|
+
})
|
|
29
|
+
).not.toContain("customer_balance");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("ticketCheckoutOffersBankTransfer reflects resolver", () => {
|
|
33
|
+
expect(
|
|
34
|
+
ticketCheckoutOffersBankTransfer({
|
|
35
|
+
installmentsEnabled: false,
|
|
36
|
+
bankTransferEnabled: true,
|
|
37
|
+
totalChargedCents: 10_000,
|
|
38
|
+
currency: "usd",
|
|
39
|
+
})
|
|
40
|
+
).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/definitions.ts
CHANGED
|
@@ -473,6 +473,8 @@ export const HTTP_CODE_FORBIDDEN = 403 as const;
|
|
|
473
473
|
export const HTTP_CODE_NOT_FOUND = 404 as const;
|
|
474
474
|
export const ERR_UNAUTHORIZED_REQUEST =
|
|
475
475
|
"Unauthorized to perform requested action. Have you logged in?" as const;
|
|
476
|
+
export const ERR_SESSION_EXPIRED =
|
|
477
|
+
"Your session has expired — please refresh or log in again." as const;
|
|
476
478
|
|
|
477
479
|
export const DEFAULT_PRISMA_TTL_SECONDS = 60 as const;
|
|
478
480
|
export const PRISMA_MEDIA_TTL_SECONDS = 60 * 5; // 5 hours
|
|
@@ -1022,6 +1024,7 @@ export type StripeLinkingStatus = {
|
|
|
1022
1024
|
cardPaymentsActive?: boolean;
|
|
1023
1025
|
klarnaPaymentsActive?: boolean;
|
|
1024
1026
|
cashappPaymentsActive?: boolean;
|
|
1027
|
+
usBankTransferPaymentsActive?: boolean;
|
|
1025
1028
|
};
|
|
1026
1029
|
|
|
1027
1030
|
export type StripeBusinessDataPublic = {
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -1217,6 +1217,9 @@ export interface InvitationExtraData extends Invitation {
|
|
|
1217
1217
|
organizerTitle?: string;
|
|
1218
1218
|
isOwnershipTransfer?: boolean;
|
|
1219
1219
|
includeQRCode?: boolean; // Controls whether QR code should be included in the invitation email
|
|
1220
|
+
sendEmail?: boolean; // When false, skip email notification (default true)
|
|
1221
|
+
existingTicketId?: string; // Link invitation to an existing ticket instead of creating a new one
|
|
1222
|
+
revoke?: boolean; // Host action: set revokedDate
|
|
1220
1223
|
}
|
|
1221
1224
|
|
|
1222
1225
|
export interface AssociatedBashExt extends AssociatedBash {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { UserRole } from "@prisma/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claims available on `req.auth` after JWT verification.
|
|
5
|
+
*
|
|
6
|
+
* This is intentionally smaller than Prisma `User`: route handlers should load
|
|
7
|
+
* full profile data from the DB when they need more than identity + authorization.
|
|
8
|
+
*/
|
|
9
|
+
export type RequestAuthUser = {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
roles: UserRole[];
|
|
13
|
+
isSuperUser?: boolean;
|
|
14
|
+
username?: string;
|
|
15
|
+
givenName?: string;
|
|
16
|
+
familyName?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const USER_ROLE_VALUES = new Set<string>(Object.values(UserRole));
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function optionalString(value: unknown): string | undefined {
|
|
26
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse and validate a `roles` JWT claim.
|
|
31
|
+
*
|
|
32
|
+
* - `undefined` → claim absent (invalid for newly issued tokens)
|
|
33
|
+
* - `null` → present but malformed
|
|
34
|
+
*/
|
|
35
|
+
export function parseUserRolesFromJwtClaim(value: unknown): UserRole[] | null | undefined {
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (value.length === 0) {
|
|
43
|
+
return [UserRole.User];
|
|
44
|
+
}
|
|
45
|
+
const roles: UserRole[] = [];
|
|
46
|
+
for (const item of value) {
|
|
47
|
+
if (typeof item !== "string" || !USER_ROLE_VALUES.has(item)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
roles.push(item as UserRole);
|
|
51
|
+
}
|
|
52
|
+
return roles;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalize a verified JWT payload into {@link RequestAuthUser}.
|
|
57
|
+
*
|
|
58
|
+
* Returns `null` when required identity claims are missing, when `roles` is
|
|
59
|
+
* absent, or when `roles` is present but invalid. All newly issued session and
|
|
60
|
+
* minimal JWTs must include a `roles` array (see signMinimalJwt / login flows).
|
|
61
|
+
*/
|
|
62
|
+
export function parseRequestAuthFromJwtPayload(decoded: unknown): RequestAuthUser | null {
|
|
63
|
+
if (!isRecord(decoded)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const id = optionalString(decoded.id);
|
|
68
|
+
const email = optionalString(decoded.email);
|
|
69
|
+
if (!id || !email) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parsedRoles = parseUserRolesFromJwtClaim(decoded.roles);
|
|
74
|
+
if (parsedRoles === null || parsedRoles === undefined) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
email,
|
|
81
|
+
roles: parsedRoles,
|
|
82
|
+
isSuperUser: decoded.isSuperUser === true ? true : undefined,
|
|
83
|
+
username: optionalString(decoded.username),
|
|
84
|
+
givenName: optionalString(decoded.givenName),
|
|
85
|
+
familyName: optionalString(decoded.familyName),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** SuperUser role or legacy `isSuperUser` flag on the session JWT. */
|
|
90
|
+
export function userHasSuperUserAccess(
|
|
91
|
+
auth: Pick<RequestAuthUser, "roles" | "isSuperUser">,
|
|
92
|
+
): boolean {
|
|
93
|
+
return auth.isSuperUser === true || auth.roles.includes(UserRole.SuperUser);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Admin or SuperUser role, or legacy `isSuperUser` flag. */
|
|
97
|
+
export function userHasAdminAccess(
|
|
98
|
+
auth: Pick<RequestAuthUser, "roles" | "isSuperUser">,
|
|
99
|
+
): boolean {
|
|
100
|
+
return (
|
|
101
|
+
auth.isSuperUser === true ||
|
|
102
|
+
auth.roles.includes(UserRole.SuperUser) ||
|
|
103
|
+
auth.roles.includes(UserRole.Admin)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -10,7 +10,16 @@ export type TicketCheckoutPaymentMethodType =
|
|
|
10
10
|
| "link"
|
|
11
11
|
| "klarna"
|
|
12
12
|
| "afterpay_clearpay"
|
|
13
|
-
| "cashapp"
|
|
13
|
+
| "cashapp"
|
|
14
|
+
| "customer_balance"
|
|
15
|
+
| "us_bank_account";
|
|
16
|
+
|
|
17
|
+
/** Service booking Checkout — card, link, and optional async bank methods. */
|
|
18
|
+
export type ServiceCheckoutPaymentMethodType =
|
|
19
|
+
| "card"
|
|
20
|
+
| "link"
|
|
21
|
+
| "customer_balance"
|
|
22
|
+
| "us_bank_account";
|
|
14
23
|
|
|
15
24
|
/** USD floors/ceilings aligned with typical Stripe BNPL eligibility (conservative). */
|
|
16
25
|
export const TICKET_BNPL_MIN_CENTS_USD = 3_500; // $35
|
|
@@ -21,6 +30,8 @@ export interface TicketBnplPaymentMethodInput {
|
|
|
21
30
|
installmentsEnabled: boolean;
|
|
22
31
|
/** Per-event host toggle (`BashEvent.cashAppEnabled`). */
|
|
23
32
|
cashAppEnabled?: boolean;
|
|
33
|
+
/** Per-event host toggle (`BashEvent.bankTransferEnabled`). */
|
|
34
|
+
bankTransferEnabled?: boolean;
|
|
24
35
|
/** Buyer total charged at checkout (tickets + guest-visible fees), in cents. */
|
|
25
36
|
totalChargedCents: number;
|
|
26
37
|
/** ISO currency lowercase, e.g. "usd". */
|
|
@@ -29,6 +40,8 @@ export interface TicketBnplPaymentMethodInput {
|
|
|
29
40
|
bnplGloballyEnabled?: boolean;
|
|
30
41
|
/** Platform kill switch (API env `TICKET_CASHAPP_ENABLED`). Default true in tests. */
|
|
31
42
|
cashAppGloballyEnabled?: boolean;
|
|
43
|
+
/** Platform kill switch (API env `TICKET_BANK_TRANSFER_ENABLED`). Default true in tests. */
|
|
44
|
+
bankTransferGloballyEnabled?: boolean;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
@@ -43,6 +56,7 @@ export function resolveTicketCheckoutPaymentMethodTypes(
|
|
|
43
56
|
|
|
44
57
|
const globallyBnplOn = input.bnplGloballyEnabled !== false;
|
|
45
58
|
const globallyCashAppOn = input.cashAppGloballyEnabled !== false;
|
|
59
|
+
const globallyBankTransferOn = input.bankTransferGloballyEnabled !== false;
|
|
46
60
|
const isUsd = input.currency.toLowerCase() === "usd";
|
|
47
61
|
const cents = input.totalChargedCents;
|
|
48
62
|
|
|
@@ -61,6 +75,15 @@ export function resolveTicketCheckoutPaymentMethodTypes(
|
|
|
61
75
|
methods.add("cashapp");
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
if (
|
|
79
|
+
globallyBankTransferOn &&
|
|
80
|
+
input.bankTransferEnabled &&
|
|
81
|
+
isUsd &&
|
|
82
|
+
cents > 0
|
|
83
|
+
) {
|
|
84
|
+
methods.add("customer_balance");
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
return [...methods];
|
|
65
88
|
}
|
|
66
89
|
|
|
@@ -77,3 +100,78 @@ export function ticketCheckoutOffersCashApp(
|
|
|
77
100
|
const methods = resolveTicketCheckoutPaymentMethodTypes(input);
|
|
78
101
|
return methods.includes("cashapp");
|
|
79
102
|
}
|
|
103
|
+
|
|
104
|
+
export function ticketCheckoutOffersBankTransfer(
|
|
105
|
+
input: TicketBnplPaymentMethodInput
|
|
106
|
+
): boolean {
|
|
107
|
+
const methods = resolveTicketCheckoutPaymentMethodTypes(input);
|
|
108
|
+
return methods.includes("customer_balance");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ServiceCheckoutPaymentMethodInput {
|
|
112
|
+
/** Per-service host toggle (`Service.bankTransferEnabled`). */
|
|
113
|
+
bankTransferEnabled?: boolean;
|
|
114
|
+
/** Per-service host toggle (`Service.achDebitEnabled`). */
|
|
115
|
+
achDebitEnabled?: boolean;
|
|
116
|
+
/** Buyer total charged at checkout, in cents. */
|
|
117
|
+
totalChargedCents: number;
|
|
118
|
+
/** ISO currency lowercase, e.g. "usd". */
|
|
119
|
+
currency: string;
|
|
120
|
+
/** Platform kill switch (API env `SERVICE_BANK_TRANSFER_ENABLED`). Default true in tests. */
|
|
121
|
+
bankTransferGloballyEnabled?: boolean;
|
|
122
|
+
/** Platform kill switch (API env `SERVICE_ACH_DEBIT_ENABLED`). Default true in tests. */
|
|
123
|
+
achDebitGloballyEnabled?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve `payment_method_types` for Stripe Checkout service booking sessions.
|
|
128
|
+
* Bank transfer and ACH are async — fulfillment waits for `checkout.session.async_payment_succeeded`.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveServiceCheckoutPaymentMethodTypes(
|
|
131
|
+
input: ServiceCheckoutPaymentMethodInput
|
|
132
|
+
): ServiceCheckoutPaymentMethodType[] {
|
|
133
|
+
const methods = new Set<ServiceCheckoutPaymentMethodType>(["card", "link"]);
|
|
134
|
+
const globallyBankTransferOn = input.bankTransferGloballyEnabled !== false;
|
|
135
|
+
const globallyAchOn = input.achDebitGloballyEnabled !== false;
|
|
136
|
+
const isUsd = input.currency.toLowerCase() === "usd";
|
|
137
|
+
const cents = input.totalChargedCents;
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
globallyBankTransferOn &&
|
|
141
|
+
input.bankTransferEnabled &&
|
|
142
|
+
isUsd &&
|
|
143
|
+
cents > 0
|
|
144
|
+
) {
|
|
145
|
+
methods.add("customer_balance");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (globallyAchOn && input.achDebitEnabled && isUsd && cents > 0) {
|
|
149
|
+
methods.add("us_bank_account");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [...methods];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function serviceCheckoutOffersBankTransfer(
|
|
156
|
+
input: ServiceCheckoutPaymentMethodInput
|
|
157
|
+
): boolean {
|
|
158
|
+
return resolveServiceCheckoutPaymentMethodTypes(input).includes(
|
|
159
|
+
"customer_balance"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function serviceCheckoutOffersAchDebit(
|
|
164
|
+
input: ServiceCheckoutPaymentMethodInput
|
|
165
|
+
): boolean {
|
|
166
|
+
return resolveServiceCheckoutPaymentMethodTypes(input).includes(
|
|
167
|
+
"us_bank_account"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function checkoutUsesAsyncBankPayment(
|
|
172
|
+
methods: ReadonlyArray<
|
|
173
|
+
TicketCheckoutPaymentMethodType | ServiceCheckoutPaymentMethodType
|
|
174
|
+
>
|
|
175
|
+
): boolean {
|
|
176
|
+
return methods.includes("customer_balance") || methods.includes("us_bank_account");
|
|
177
|
+
}
|