@bash-app/bash-common 30.429.0 → 30.431.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__/definitionsHelpers.test.js +1 -0
- package/dist/__tests__/definitionsHelpers.test.js.map +1 -1
- package/dist/bashFeedTypes.d.ts +3 -0
- package/dist/bashFeedTypes.d.ts.map +1 -1
- package/dist/definitions.d.ts +1 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +2 -0
- package/dist/definitions.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/utils/__tests__/feedPostSocialLinks.test.d.ts +2 -0
- package/dist/utils/__tests__/feedPostSocialLinks.test.d.ts.map +1 -0
- package/dist/utils/__tests__/feedPostSocialLinks.test.js +46 -0
- package/dist/utils/__tests__/feedPostSocialLinks.test.js.map +1 -0
- package/dist/utils/__tests__/paymentUtils.test.js +12 -1
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
- package/dist/utils/feedPostSocialLinks.d.ts +26 -0
- package/dist/utils/feedPostSocialLinks.d.ts.map +1 -0
- package/dist/utils/feedPostSocialLinks.js +159 -0
- package/dist/utils/feedPostSocialLinks.js.map +1 -0
- package/dist/utils/paymentUtils.d.ts +2 -0
- package/dist/utils/paymentUtils.d.ts.map +1 -1
- package/dist/utils/paymentUtils.js +16 -3
- package/dist/utils/paymentUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +30 -2
- package/src/__tests__/definitionsHelpers.test.ts +1 -0
- package/src/bashFeedTypes.ts +4 -0
- package/src/definitions.ts +3 -0
- package/src/index.ts +1 -0
- package/src/utils/__tests__/feedPostSocialLinks.test.ts +62 -0
- package/src/utils/__tests__/paymentUtils.test.ts +13 -0
- package/src/utils/feedPostSocialLinks.ts +174 -0
- package/src/utils/paymentUtils.ts +21 -3
package/src/bashFeedTypes.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
Privacy,
|
|
5
5
|
} from "@prisma/client";
|
|
6
6
|
|
|
7
|
+
import type { FeedPostSocialLink } from "./utils/feedPostSocialLinks.js";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Bash snippet on a feed post (matches typical `linkedBashInclude` / shaping output).
|
|
9
11
|
* `activeOffers` replaces raw `ticketTiers` after API media-tree shaping.
|
|
@@ -122,6 +124,7 @@ export interface BashFeedDraftView {
|
|
|
122
124
|
mediaIds: string[];
|
|
123
125
|
mediaUrls: string[];
|
|
124
126
|
isAnonymous: boolean;
|
|
127
|
+
socialLinks?: FeedPostSocialLink[] | null;
|
|
125
128
|
bash: { id: string; title: string | null; coverPhoto: string | null } | null;
|
|
126
129
|
service: { id: string; serviceName: string | null; coverPhoto: string | null } | null;
|
|
127
130
|
}
|
|
@@ -213,6 +216,7 @@ export interface BashFeedPostData {
|
|
|
213
216
|
content: string | null;
|
|
214
217
|
mediaIds: string[];
|
|
215
218
|
mediaUrls?: string[];
|
|
219
|
+
socialLinks?: FeedPostSocialLink[] | null;
|
|
216
220
|
isFeatured?: boolean;
|
|
217
221
|
isAnonymous?: boolean;
|
|
218
222
|
createdAt: string;
|
package/src/definitions.ts
CHANGED
|
@@ -835,6 +835,7 @@ export type FilterFields = {
|
|
|
835
835
|
incomeRange: string[];
|
|
836
836
|
hasOpenTasks: boolean; // Boolean filter for events with open volunteer tasks
|
|
837
837
|
acceptsBashPoints: boolean; // Boolean filter for events with BashPoints-priced tickets
|
|
838
|
+
acceptsBashPass: boolean; // Boolean filter for events with a BashPass-eligible ticket
|
|
838
839
|
ambashadorOnly: boolean; // Boolean filter for events created by Ambashadors
|
|
839
840
|
};
|
|
840
841
|
|
|
@@ -854,6 +855,7 @@ export const EMPTY_FILTER_FIELDS: FilterFields = {
|
|
|
854
855
|
incomeRange: [],
|
|
855
856
|
hasOpenTasks: false,
|
|
856
857
|
acceptsBashPoints: false,
|
|
858
|
+
acceptsBashPass: false,
|
|
857
859
|
ambashadorOnly: false,
|
|
858
860
|
};
|
|
859
861
|
|
|
@@ -874,6 +876,7 @@ export function createEmptyFilterFields(): FilterFields {
|
|
|
874
876
|
incomeRange: [],
|
|
875
877
|
hasOpenTasks: false,
|
|
876
878
|
acceptsBashPoints: false,
|
|
879
|
+
acceptsBashPass: false,
|
|
877
880
|
ambashadorOnly: false,
|
|
878
881
|
};
|
|
879
882
|
}
|
package/src/index.ts
CHANGED
|
@@ -394,6 +394,7 @@ export * from "./utils/discountEngine/bestPriceResolver.js";
|
|
|
394
394
|
export * from "./utils/discountEngine/eligibilityValidator.js";
|
|
395
395
|
export * from "./utils/nonprofitUtils.js";
|
|
396
396
|
export * from "./utils/objUtils.js";
|
|
397
|
+
export * from "./utils/feedPostSocialLinks.js";
|
|
397
398
|
export * from "./utils/paymentUtils.js";
|
|
398
399
|
export * from "./utils/promoCodesUtils.js";
|
|
399
400
|
export * from "./utils/qrCodeUtils.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFeedPostSocialLinks,
|
|
3
|
+
providerLinksFromOrganization,
|
|
4
|
+
withoutPosterSocialLinks,
|
|
5
|
+
} from "../feedPostSocialLinks.js";
|
|
6
|
+
|
|
7
|
+
describe("parseFeedPostSocialLinks", () => {
|
|
8
|
+
it("accepts https links and upgrades a bare host", () => {
|
|
9
|
+
const result = parseFeedPostSocialLinks([
|
|
10
|
+
{ platform: "Website", url: "example.com/book" },
|
|
11
|
+
{ platform: "Instagram", url: "@nightowl" },
|
|
12
|
+
]);
|
|
13
|
+
expect("links" in result).toBe(true);
|
|
14
|
+
if (!("links" in result)) return;
|
|
15
|
+
expect(result.links[0]?.url).toBe("https://example.com/book");
|
|
16
|
+
expect(result.links[1]?.url).toBe("https://instagram.com/nightowl");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("rejects scripts and too many links", () => {
|
|
20
|
+
expect(parseFeedPostSocialLinks([{ platform: "Website", url: "javascript:alert(1)" }])).toEqual({
|
|
21
|
+
error: "Links need a normal https address.",
|
|
22
|
+
});
|
|
23
|
+
expect(
|
|
24
|
+
parseFeedPostSocialLinks(
|
|
25
|
+
Array.from({ length: 6 }, () => ({
|
|
26
|
+
platform: "Website",
|
|
27
|
+
url: "https://example.com",
|
|
28
|
+
}))
|
|
29
|
+
)
|
|
30
|
+
).toEqual({ error: "Add up to 5 links." });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("withoutPosterSocialLinks", () => {
|
|
35
|
+
it("drops links that match the poster's own profile", () => {
|
|
36
|
+
const links = parseFeedPostSocialLinks([
|
|
37
|
+
{ platform: "Instagram", url: "https://instagram.com/me" },
|
|
38
|
+
{ platform: "Website", url: "https://venue.example" },
|
|
39
|
+
]);
|
|
40
|
+
if (!("links" in links)) throw new Error("expected links");
|
|
41
|
+
expect(
|
|
42
|
+
withoutPosterSocialLinks(links.links, ["https://www.instagram.com/me/"])
|
|
43
|
+
).toEqual([
|
|
44
|
+
{ platform: "Website", url: "https://venue.example/" },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("providerLinksFromOrganization", () => {
|
|
50
|
+
it("reads website and known social keys", () => {
|
|
51
|
+
expect(
|
|
52
|
+
providerLinksFromOrganization(
|
|
53
|
+
{ instagram: "@club", twitter: "https://x.com/club" },
|
|
54
|
+
"https://club.example"
|
|
55
|
+
)
|
|
56
|
+
).toEqual([
|
|
57
|
+
{ platform: "Website", url: "https://club.example/" },
|
|
58
|
+
{ platform: "Instagram", url: "https://instagram.com/club" },
|
|
59
|
+
{ platform: "X", url: "https://x.com/club" },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
calculateStripeProcessingFeeCents,
|
|
15
15
|
calculateTotalPriceWithoutTax,
|
|
16
16
|
calculateTotalBashPoints,
|
|
17
|
+
calculateMixedPricingBashPoints,
|
|
17
18
|
calculateStripeDonationFee,
|
|
18
19
|
calculateServiceFeeWithMembershipDiscount,
|
|
19
20
|
calculatePartnerFeeWithMembershipDiscount,
|
|
@@ -398,6 +399,18 @@ describe("PaymentUtils - Core Functions", () => {
|
|
|
398
399
|
const ticketList = new Map([["t1", [{ numberOfTickets: 1, ticketDateTime: new Date() }]]]);
|
|
399
400
|
expect(calculateTotalBashPoints(tiers, ticketList)).toBe(0);
|
|
400
401
|
});
|
|
402
|
+
|
|
403
|
+
test("mixed tiers charge dollars and BashPoints", () => {
|
|
404
|
+
const tiers = [
|
|
405
|
+
{ id: "t1", price: 500, pricingType: "MIXED", priceBashPoints: 50 },
|
|
406
|
+
] as any;
|
|
407
|
+
const ticketList = new Map([
|
|
408
|
+
["t1", [{ numberOfTickets: 2, ticketDateTime: new Date() }]],
|
|
409
|
+
]);
|
|
410
|
+
expect(calculateTotalPriceWithoutTax(tiers, ticketList)).toBe(1000);
|
|
411
|
+
expect(calculateTotalBashPoints(tiers, ticketList)).toBe(100);
|
|
412
|
+
expect(calculateMixedPricingBashPoints(tiers, ticketList)).toBe(100);
|
|
413
|
+
});
|
|
401
414
|
});
|
|
402
415
|
|
|
403
416
|
describe("Stripe donation fee", () => {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links stored on a service-mode BashFeed post. Not written back to a profile.
|
|
3
|
+
*/
|
|
4
|
+
export const FEED_POST_SOCIAL_PLATFORMS = [
|
|
5
|
+
"Instagram",
|
|
6
|
+
"LinkedIn",
|
|
7
|
+
"X",
|
|
8
|
+
"Website",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export const FEED_POST_SOCIAL_LINK_MAX = 5;
|
|
12
|
+
|
|
13
|
+
export type FeedPostSocialPlatform = (typeof FEED_POST_SOCIAL_PLATFORMS)[number];
|
|
14
|
+
|
|
15
|
+
export type FeedPostSocialLink = {
|
|
16
|
+
platform: FeedPostSocialPlatform;
|
|
17
|
+
url: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isPlatform(value: string): value is FeedPostSocialPlatform {
|
|
22
|
+
return (FEED_POST_SOCIAL_PLATFORMS as readonly string[]).includes(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** https URL, or null when the value is not a normal web link. */
|
|
26
|
+
export function toFeedPostHttpsUrl(raw: string): string | null {
|
|
27
|
+
const trimmed = raw.trim();
|
|
28
|
+
if (!trimmed) return null;
|
|
29
|
+
if (/^(javascript|data|file|mailto|tel):/i.test(trimmed)) return null;
|
|
30
|
+
|
|
31
|
+
let candidate = trimmed;
|
|
32
|
+
if (/^http:\/\//i.test(candidate)) {
|
|
33
|
+
candidate = `https://${candidate.slice("http://".length)}`;
|
|
34
|
+
} else if (!/^https:\/\//i.test(candidate)) {
|
|
35
|
+
if (candidate.startsWith("@")) return null;
|
|
36
|
+
candidate = `https://${candidate.replace(/^\/+/, "")}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(candidate);
|
|
41
|
+
if (url.protocol !== "https:") return null;
|
|
42
|
+
if (!url.hostname || !url.hostname.includes(".")) return null;
|
|
43
|
+
return url.toString();
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleToHttps(platform: FeedPostSocialPlatform, raw: string): string | null {
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
const handle = trimmed.replace(/^@/, "");
|
|
52
|
+
if (!handle || /\s/.test(handle)) return null;
|
|
53
|
+
if (platform === "Instagram") return toFeedPostHttpsUrl(`https://instagram.com/${handle}`);
|
|
54
|
+
if (platform === "X") return toFeedPostHttpsUrl(`https://x.com/${handle}`);
|
|
55
|
+
if (platform === "LinkedIn") return toFeedPostHttpsUrl(`https://www.linkedin.com/in/${handle}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function parseFeedPostSocialLinks(
|
|
60
|
+
input: unknown
|
|
61
|
+
): { links: FeedPostSocialLink[] } | { error: string } {
|
|
62
|
+
if (input == null) return { links: [] };
|
|
63
|
+
if (!Array.isArray(input)) {
|
|
64
|
+
return { error: "Social links must be a list." };
|
|
65
|
+
}
|
|
66
|
+
if (input.length > FEED_POST_SOCIAL_LINK_MAX) {
|
|
67
|
+
return { error: `Add up to ${FEED_POST_SOCIAL_LINK_MAX} links.` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const links: FeedPostSocialLink[] = [];
|
|
71
|
+
for (const row of input) {
|
|
72
|
+
if (!row || typeof row !== "object") {
|
|
73
|
+
return { error: "Each social link needs a platform and a URL." };
|
|
74
|
+
}
|
|
75
|
+
const record = row as { platform?: unknown; url?: unknown; label?: unknown };
|
|
76
|
+
const platform = typeof record.platform === "string" ? record.platform.trim() : "";
|
|
77
|
+
const urlRaw = typeof record.url === "string" ? record.url : "";
|
|
78
|
+
if (!isPlatform(platform)) {
|
|
79
|
+
return { error: "Use Instagram, LinkedIn, X, or a website." };
|
|
80
|
+
}
|
|
81
|
+
const url =
|
|
82
|
+
toFeedPostHttpsUrl(urlRaw) ??
|
|
83
|
+
(urlRaw.trim().startsWith("@") ? handleToHttps(platform, urlRaw) : null);
|
|
84
|
+
if (!url) {
|
|
85
|
+
return { error: "Links need a normal https address." };
|
|
86
|
+
}
|
|
87
|
+
const label =
|
|
88
|
+
typeof record.label === "string" && record.label.trim()
|
|
89
|
+
? record.label.trim().slice(0, 40)
|
|
90
|
+
: undefined;
|
|
91
|
+
links.push(label ? { platform, url, label } : { platform, url });
|
|
92
|
+
}
|
|
93
|
+
return { links };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function urlKey(url: string): string {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = new URL(url);
|
|
99
|
+
const host = parsed.hostname.replace(/^www\./, "").toLowerCase();
|
|
100
|
+
const path = parsed.pathname.replace(/\/$/, "").toLowerCase();
|
|
101
|
+
return `${host}${path}`;
|
|
102
|
+
} catch {
|
|
103
|
+
return url.trim().toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Drop links that match the poster's own profile URLs. */
|
|
108
|
+
export function withoutPosterSocialLinks(
|
|
109
|
+
links: FeedPostSocialLink[],
|
|
110
|
+
posterUrls: Array<string | null | undefined>
|
|
111
|
+
): FeedPostSocialLink[] {
|
|
112
|
+
const blocked = new Set<string>();
|
|
113
|
+
for (const raw of posterUrls) {
|
|
114
|
+
if (!raw?.trim()) continue;
|
|
115
|
+
const https = toFeedPostHttpsUrl(raw);
|
|
116
|
+
if (https) blocked.add(urlKey(https));
|
|
117
|
+
}
|
|
118
|
+
if (blocked.size === 0) return links;
|
|
119
|
+
return links.filter((link) => !blocked.has(urlKey(link.url)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pushUnique(
|
|
123
|
+
links: FeedPostSocialLink[],
|
|
124
|
+
platform: FeedPostSocialPlatform,
|
|
125
|
+
raw: string
|
|
126
|
+
): void {
|
|
127
|
+
if (links.length >= FEED_POST_SOCIAL_LINK_MAX) return;
|
|
128
|
+
const url =
|
|
129
|
+
toFeedPostHttpsUrl(raw) ??
|
|
130
|
+
(raw.trim().startsWith("@") ? handleToHttps(platform, raw) : null);
|
|
131
|
+
if (!url) return;
|
|
132
|
+
if (links.some((link) => link.platform === platform && urlKey(link.url) === urlKey(url))) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
links.push({ platform, url });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Prefill from an organization's stored social JSON and website.
|
|
140
|
+
* Accepts `{ instagram, linkedin, x, twitter, website }` or an array of `{ platform, url }`.
|
|
141
|
+
*/
|
|
142
|
+
export function providerLinksFromOrganization(
|
|
143
|
+
socialLinks: unknown,
|
|
144
|
+
websiteUrl: string | null | undefined
|
|
145
|
+
): FeedPostSocialLink[] {
|
|
146
|
+
const links: FeedPostSocialLink[] = [];
|
|
147
|
+
if (websiteUrl?.trim()) {
|
|
148
|
+
pushUnique(links, "Website", websiteUrl);
|
|
149
|
+
}
|
|
150
|
+
if (!socialLinks) return links;
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(socialLinks)) {
|
|
153
|
+
const parsed = parseFeedPostSocialLinks(socialLinks);
|
|
154
|
+
if ("links" in parsed) {
|
|
155
|
+
for (const link of parsed.links) pushUnique(links, link.platform, link.url);
|
|
156
|
+
}
|
|
157
|
+
return links;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof socialLinks !== "object") return links;
|
|
161
|
+
const record = socialLinks as Record<string, unknown>;
|
|
162
|
+
const pairs: Array<[FeedPostSocialPlatform, unknown]> = [
|
|
163
|
+
["Instagram", record.instagram ?? record.Instagram],
|
|
164
|
+
["LinkedIn", record.linkedin ?? record.LinkedIn],
|
|
165
|
+
["X", record.x ?? record.X ?? record.twitter ?? record.Twitter],
|
|
166
|
+
["Website", record.website ?? record.Website ?? record.url],
|
|
167
|
+
];
|
|
168
|
+
for (const [platform, value] of pairs) {
|
|
169
|
+
if (typeof value === "string" && value.trim()) {
|
|
170
|
+
pushUnique(links, platform, value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return links;
|
|
174
|
+
}
|
|
@@ -96,7 +96,7 @@ export function calculateTotalPriceWithoutTax(
|
|
|
96
96
|
): number {
|
|
97
97
|
let total = 0;
|
|
98
98
|
ticketTiers.forEach((tier: TicketTier) => {
|
|
99
|
-
//
|
|
99
|
+
// USD and mixed tiers charge the dollar price. Points-only tiers do not.
|
|
100
100
|
if (tier.pricingType !== "BASHPOINTS") {
|
|
101
101
|
const ticketListNumAndDate = ticketList.get(tier.id);
|
|
102
102
|
ticketListNumAndDate?.forEach((numAndDate: NumberOfTicketsForDate) => {
|
|
@@ -120,8 +120,10 @@ export function calculateTotalBashPoints(
|
|
|
120
120
|
): number {
|
|
121
121
|
let total = 0;
|
|
122
122
|
ticketTiers.forEach((tier: TicketTier) => {
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
if (
|
|
124
|
+
(tier.pricingType === "BASHPOINTS" || tier.pricingType === "MIXED") &&
|
|
125
|
+
tier.priceBashPoints
|
|
126
|
+
) {
|
|
125
127
|
const ticketListNumAndDate = ticketList.get(tier.id);
|
|
126
128
|
ticketListNumAndDate?.forEach((numAndDate: NumberOfTicketsForDate) => {
|
|
127
129
|
total += tier.priceBashPoints! * (numAndDate.numberOfTickets ?? 0);
|
|
@@ -131,6 +133,22 @@ export function calculateTotalBashPoints(
|
|
|
131
133
|
return total;
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
/** BashPoints due on tiers that also charge a card (pricingType MIXED). */
|
|
137
|
+
export function calculateMixedPricingBashPoints(
|
|
138
|
+
ticketTiers: TicketTier[],
|
|
139
|
+
ticketList: Map<string, NumberOfTicketsForDate[]>
|
|
140
|
+
): number {
|
|
141
|
+
let total = 0;
|
|
142
|
+
ticketTiers.forEach((tier: TicketTier) => {
|
|
143
|
+
if (tier.pricingType !== "MIXED" || !tier.priceBashPoints) return;
|
|
144
|
+
const ticketListNumAndDate = ticketList.get(tier.id);
|
|
145
|
+
ticketListNumAndDate?.forEach((numAndDate: NumberOfTicketsForDate) => {
|
|
146
|
+
total += tier.priceBashPoints! * (numAndDate.numberOfTickets ?? 0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
return total;
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
export function convertCentsToDollars(
|
|
135
153
|
price: number | undefined | null
|
|
136
154
|
): number {
|