@bash-app/bash-common 30.186.0 → 30.193.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__/stripeListingSubscriptionMessages.test.d.ts +2 -0
- package/dist/__tests__/stripeListingSubscriptionMessages.test.d.ts.map +1 -0
- package/dist/__tests__/stripeListingSubscriptionMessages.test.js +10 -0
- package/dist/__tests__/stripeListingSubscriptionMessages.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/stripeListingSubscriptionMessages.d.ts +7 -0
- package/dist/stripeListingSubscriptionMessages.d.ts.map +1 -0
- package/dist/stripeListingSubscriptionMessages.js +13 -0
- package/dist/stripeListingSubscriptionMessages.js.map +1 -0
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts +6 -0
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.d.ts.map +1 -0
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js +104 -0
- package/dist/utils/__tests__/cancellationPolicyRefundResolver.test.js.map +1 -0
- package/dist/utils/addressUtils.d.ts.map +1 -1
- package/dist/utils/addressUtils.js +157 -54
- package/dist/utils/addressUtils.js.map +1 -1
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js +6 -2
- package/dist/utils/discountEngine/__tests__/eligibilityValidator.test.js.map +1 -1
- package/dist/utils/mediaClientDefaults.d.ts +7 -0
- package/dist/utils/mediaClientDefaults.d.ts.map +1 -0
- package/dist/utils/mediaClientDefaults.js +7 -0
- package/dist/utils/mediaClientDefaults.js.map +1 -0
- package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.d.ts +2 -0
- package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.js +18 -0
- package/dist/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.js.map +1 -0
- package/dist/utils/service/cancellationPolicyRefundResolver.d.ts +30 -0
- package/dist/utils/service/cancellationPolicyRefundResolver.d.ts.map +1 -0
- package/dist/utils/service/cancellationPolicyRefundResolver.js +82 -0
- package/dist/utils/service/cancellationPolicyRefundResolver.js.map +1 -0
- package/dist/utils/service/serviceUtils.d.ts +7 -0
- package/dist/utils/service/serviceUtils.d.ts.map +1 -1
- package/dist/utils/service/serviceUtils.js +173 -11
- package/dist/utils/service/serviceUtils.js.map +1 -1
- package/dist/utils/slugUtils.d.ts +5 -0
- package/dist/utils/slugUtils.d.ts.map +1 -1
- package/dist/utils/slugUtils.js +12 -0
- package/dist/utils/slugUtils.js.map +1 -1
- package/package.json +3 -2
- package/prisma/schema.prisma +6 -0
- package/src/__tests__/stripeListingSubscriptionMessages.test.ts +20 -0
- package/src/index.ts +3 -0
- package/src/stripeListingSubscriptionMessages.ts +18 -0
- package/src/utils/addressUtils.ts +175 -59
- package/src/utils/discountEngine/__tests__/eligibilityValidator.test.ts +6 -2
- package/src/utils/mediaClientDefaults.ts +6 -0
- package/src/utils/service/__tests__/partnerServiceProfileWizardPaymentMethod.test.ts +29 -0
- package/src/utils/service/cancellationPolicyRefundResolver.ts +112 -0
- package/src/utils/service/serviceUtils.ts +215 -15
- package/src/utils/slugUtils.ts +16 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slugUtils.d.ts","sourceRoot":"","sources":["../../src/utils/slugUtils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slugUtils.d.ts","sourceRoot":"","sources":["../../src/utils/slugUtils.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EACvE,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAajB;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC9B,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAqB5F;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGlD"}
|
package/dist/utils/slugUtils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import slugify from 'slugify';
|
|
2
|
+
import { BASH_DETAIL_URL } from '../definitions.js';
|
|
2
3
|
export function generateSlug(title) {
|
|
3
4
|
return slugify(title, {
|
|
4
5
|
lower: true,
|
|
@@ -22,6 +23,17 @@ export async function generateUniqueSlug(title, checkSlugExists, excludeId) {
|
|
|
22
23
|
export function generateBashDetailUrl(bashEventId, slug) {
|
|
23
24
|
return `/bash/${bashEventId}-${slug}`;
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Public event path for links shared to social / QR (canonical frontend URL).
|
|
28
|
+
* With a slug: `/bash/{id}-{slug}`; without: `/bash/{id}` (legacy id-only segment).
|
|
29
|
+
*/
|
|
30
|
+
export function getPublicBashDetailPath(bashEventId, slug) {
|
|
31
|
+
const s = slug?.trim();
|
|
32
|
+
if (s) {
|
|
33
|
+
return generateBashDetailUrl(bashEventId, s);
|
|
34
|
+
}
|
|
35
|
+
return `${BASH_DETAIL_URL}/${bashEventId}`;
|
|
36
|
+
}
|
|
25
37
|
export function parseBashUrlParams(param) {
|
|
26
38
|
// Expected format: "id-slug" where id is first part before first dash
|
|
27
39
|
const dashIndex = param.indexOf('-');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slugUtils.js","sourceRoot":"","sources":["../../src/utils/slugUtils.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"slugUtils.js","sourceRoot":"","sources":["../../src/utils/slugUtils.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,OAAO,CAAC,KAAK,EAAE;QACpB,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,gBAAgB;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAa,EACb,eAAuE,EACvE,SAAkB;IAElB,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACtD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,GAAG,GAAG,QAAQ,IAAI,OAAO,EAAE,CAAC;QAChC,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,WAAmB,EAAE,IAAY;IACrE,OAAO,SAAS,WAAW,IAAI,IAAI,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,WAAmB,EACnB,IAA+B;IAE/B,MAAM,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE,CAAC;QACN,OAAO,qBAAqB,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,eAAe,IAAI,WAAW,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,sEAAsE;IACtE,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAErC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,0BAA0B;QAC1B,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACnC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAE5C,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sBAAsB;IACtB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,sEAAsE;IACtE,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bash-app/bash-common",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.193.0",
|
|
4
4
|
"description": "Common data and scripts to use on the frontend and backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
+
"test": "jest --config jest.config.cjs",
|
|
9
10
|
"build": "npm run generate && npm run tsc",
|
|
10
11
|
"generate": "prisma generate",
|
|
11
12
|
"db": "prisma generate && prisma db push",
|
|
12
13
|
"tsc": "NODE_OPTIONS='--max-old-space-size=16384' tsc",
|
|
13
14
|
"typecheck": "NODE_OPTIONS='--max-old-space-size=16384' tsc --noEmit",
|
|
14
|
-
"block-malicious": "
|
|
15
|
+
"block-malicious": "./scripts/block-malicious-patterns.sh .",
|
|
15
16
|
"prepublishOnly": "npm run build"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
package/prisma/schema.prisma
CHANGED
|
@@ -4367,6 +4367,12 @@ enum ServiceCancellationPolicy {
|
|
|
4367
4367
|
VendorFlexible
|
|
4368
4368
|
VendorStandard
|
|
4369
4369
|
VendorStrict
|
|
4370
|
+
ExhibitorFlexible
|
|
4371
|
+
ExhibitorStandard
|
|
4372
|
+
ExhibitorStrict
|
|
4373
|
+
SponsorFlexible
|
|
4374
|
+
SponsorStandard
|
|
4375
|
+
SponsorStrict
|
|
4370
4376
|
}
|
|
4371
4377
|
|
|
4372
4378
|
enum UserSubscriptionStatus {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LISTING_SUBSCRIPTION_PAYMENT_ALREADY_LINKING_USER_MESSAGE,
|
|
3
|
+
userFacingMessageForListingSubscriptionStripeError,
|
|
4
|
+
} from "../stripeListingSubscriptionMessages";
|
|
5
|
+
|
|
6
|
+
describe("userFacingMessageForListingSubscriptionStripeError", () => {
|
|
7
|
+
it("maps Stripe already-attached wording to actionable copy", () => {
|
|
8
|
+
expect(
|
|
9
|
+
userFacingMessageForListingSubscriptionStripeError(
|
|
10
|
+
"The payment method has already been attached to a customer."
|
|
11
|
+
)
|
|
12
|
+
).toBe(LISTING_SUBSCRIPTION_PAYMENT_ALREADY_LINKING_USER_MESSAGE);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("passes through other messages", () => {
|
|
16
|
+
expect(
|
|
17
|
+
userFacingMessageForListingSubscriptionStripeError("Something else went wrong.")
|
|
18
|
+
).toBe("Something else went wrong.");
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./definitions.js";
|
|
2
|
+
export * from "./stripeListingSubscriptionMessages.js";
|
|
2
3
|
export * from "./extendedSchemas.js";
|
|
3
4
|
export * from "./bashFeedTypes.js";
|
|
4
5
|
export * from "./membershipDefinitions.js";
|
|
@@ -32,6 +33,7 @@ export * from "./utils/recurrenceUtils.js";
|
|
|
32
33
|
export * from "./utils/service/attendeeOptionUtils.js";
|
|
33
34
|
export * from "./utils/service/regexUtils.js";
|
|
34
35
|
export * from "./utils/service/serviceUtils.js";
|
|
36
|
+
export * from "./utils/service/cancellationPolicyRefundResolver.js";
|
|
35
37
|
// Venue utils moved to paymentUtils.ts for better organization
|
|
36
38
|
export * from "./utils/slugUtils.js";
|
|
37
39
|
export * from "./utils/sortUtils.js";
|
|
@@ -45,6 +47,7 @@ export * from "./utils/blogUtils.js";
|
|
|
45
47
|
export * from "./utils/entityUtils.js";
|
|
46
48
|
export * from "./utils/generalDateTimeUtils.js";
|
|
47
49
|
export * from "./utils/luxonUtils.js";
|
|
50
|
+
export * from "./utils/mediaClientDefaults.js";
|
|
48
51
|
export * from "./utils/mathUtils.js";
|
|
49
52
|
export * from "./utils/birthdayExperienceQuality.js";
|
|
50
53
|
export * from "./utils/service/apiServiceBookingApiUtils.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing copy for listing-subscription (Connect billing customer) payment flows.
|
|
3
|
+
* Kept in bash-common so api and bash-app stay aligned without casts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const STRIPE_ALREADY_ATTACHED_SUBSTR = /already been attached to a customer/i;
|
|
7
|
+
|
|
8
|
+
export const LISTING_SUBSCRIPTION_PAYMENT_ALREADY_LINKING_USER_MESSAGE =
|
|
9
|
+
"Your card is still linking to billing. Wait a few seconds and tap Confirm again.";
|
|
10
|
+
|
|
11
|
+
export function userFacingMessageForListingSubscriptionStripeError(
|
|
12
|
+
raw: string
|
|
13
|
+
): string {
|
|
14
|
+
if (STRIPE_ALREADY_ATTACHED_SUBSTR.test(raw)) {
|
|
15
|
+
return LISTING_SUBSCRIPTION_PAYMENT_ALREADY_LINKING_USER_MESSAGE;
|
|
16
|
+
}
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
@@ -14,17 +14,98 @@ export function addressValuesToDatabaseAddressString(addressValues: IAddress): s
|
|
|
14
14
|
return [place, street, city, state, zipCode, country].join(ADDRESS_DELIM);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Older Where step saved locations with a " • " join and dropped empty segments,
|
|
19
|
+
* so the value no longer contained `|` and the full address was mis-parsed as `place`.
|
|
20
|
+
*/
|
|
21
|
+
function parseLegacyBulletDelimitedAddress(trimmed: string): IAddress | null {
|
|
22
|
+
if (!trimmed.includes(' • ')) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const parts = trimmed.split(' • ').map((p) => p.trim()).filter((p) => p.length > 0);
|
|
26
|
+
const zipLike = (s: string) => /^\d{5}(-\d{4})?$/.test(s);
|
|
27
|
+
|
|
28
|
+
if (parts.length === 6) {
|
|
29
|
+
return {
|
|
30
|
+
place: parts[0] ?? '',
|
|
31
|
+
street: parts[1] ?? '',
|
|
32
|
+
city: parts[2] ?? '',
|
|
33
|
+
state: parts[3] ?? '',
|
|
34
|
+
zipCode: parts[4] ?? '',
|
|
35
|
+
country: parts[5] ?? 'USA',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const zip3 = parts[3] ?? '';
|
|
40
|
+
const zip4 = parts[4] ?? '';
|
|
41
|
+
// Typical corruption: empty place dropped → "street • city • state • zip • USA"
|
|
42
|
+
if (
|
|
43
|
+
parts.length === 5 &&
|
|
44
|
+
zipLike(zip3) &&
|
|
45
|
+
/^(USA|United States)$/i.test(zip4)
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
place: '',
|
|
49
|
+
street: parts[0] ?? '',
|
|
50
|
+
city: parts[1] ?? '',
|
|
51
|
+
state: parts[2] ?? '',
|
|
52
|
+
zipCode: parts[3] ?? '',
|
|
53
|
+
country: 'USA',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// "place • street • city • state • zip" (no country segment)
|
|
58
|
+
if (parts.length === 5 && zipLike(zip4)) {
|
|
59
|
+
return {
|
|
60
|
+
place: parts[0] ?? '',
|
|
61
|
+
street: parts[1] ?? '',
|
|
62
|
+
city: parts[2] ?? '',
|
|
63
|
+
state: parts[3] ?? '',
|
|
64
|
+
zipCode: parts[4] ?? '',
|
|
65
|
+
country: 'USA',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (parts.length === 4 && zipLike(parts[3] ?? '')) {
|
|
70
|
+
return {
|
|
71
|
+
place: '',
|
|
72
|
+
street: parts[0] ?? '',
|
|
73
|
+
city: parts[1] ?? '',
|
|
74
|
+
state: parts[2] ?? '',
|
|
75
|
+
zipCode: parts[3] ?? '',
|
|
76
|
+
country: 'USA',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
17
83
|
export function databaseAddressStringToAddressValues(addressString: string | undefined | null): IAddress {
|
|
18
84
|
if (addressString) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
85
|
+
const trimmed = addressString.trim();
|
|
86
|
+
if (trimmed.includes(ADDRESS_DELIM)) {
|
|
87
|
+
const addressArray = trimmed.split(ADDRESS_DELIM);
|
|
88
|
+
return {
|
|
89
|
+
place: addressArray[0] ?? '',
|
|
90
|
+
street: addressArray[1] ?? '',
|
|
91
|
+
city: addressArray[2] ?? '',
|
|
92
|
+
state: addressArray[3] ?? '',
|
|
93
|
+
zipCode: addressArray[4] ?? '',
|
|
94
|
+
country: addressArray[5]?.trim() || 'USA',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const legacyBullet = parseLegacyBulletDelimitedAddress(trimmed);
|
|
98
|
+
if (legacyBullet) {
|
|
99
|
+
return legacyBullet;
|
|
27
100
|
}
|
|
101
|
+
return {
|
|
102
|
+
place: trimmed,
|
|
103
|
+
street: '',
|
|
104
|
+
city: '',
|
|
105
|
+
state: '',
|
|
106
|
+
zipCode: '',
|
|
107
|
+
country: '',
|
|
108
|
+
};
|
|
28
109
|
}
|
|
29
110
|
return { place: '', street: '', city: '', state: '', zipCode: '', country: '' };
|
|
30
111
|
}
|
|
@@ -46,54 +127,45 @@ export function formatLocalityForCardDisplay(
|
|
|
46
127
|
}
|
|
47
128
|
|
|
48
129
|
export function databaseAddressStringToOneLineString(addressString: string | undefined | null): string {
|
|
49
|
-
if (addressString) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return address.place.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let addressArr = address.place ? [address.place] : [];
|
|
66
|
-
|
|
67
|
-
// Add non-empty and non-undefined parts
|
|
68
|
-
if (address.street && address.street !== 'undefined') addressArr.push(address.street);
|
|
69
|
-
if (address.city && address.city !== 'undefined') addressArr.push(address.city);
|
|
70
|
-
if (address.state && address.state !== 'undefined') addressArr.push(address.state);
|
|
71
|
-
|
|
72
|
-
const addressStr = addressArr.filter(str => !!str && str !== 'undefined').join(', ');
|
|
73
|
-
|
|
74
|
-
// Only add zip code if it exists and isn't 'undefined'
|
|
75
|
-
return address.zipCode && address.zipCode !== 'undefined'
|
|
76
|
-
? `${addressStr} ${address.zipCode}`
|
|
77
|
-
: addressStr;
|
|
130
|
+
if (!addressString) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
const address = databaseAddressStringToAddressValues(addressString);
|
|
134
|
+
|
|
135
|
+
// If only place exists, just return it (includes place-only strings with no delimiters)
|
|
136
|
+
if (
|
|
137
|
+
address.place &&
|
|
138
|
+
(!address.street || address.street === 'undefined') &&
|
|
139
|
+
(!address.city || address.city === 'undefined') &&
|
|
140
|
+
(!address.state || address.state === 'undefined')
|
|
141
|
+
) {
|
|
142
|
+
return address.place.trim();
|
|
78
143
|
}
|
|
79
|
-
|
|
144
|
+
|
|
145
|
+
let addressArr = address.place ? [address.place] : [];
|
|
146
|
+
|
|
147
|
+
if (address.street && address.street !== 'undefined') addressArr.push(address.street);
|
|
148
|
+
if (address.city && address.city !== 'undefined') addressArr.push(address.city);
|
|
149
|
+
if (address.state && address.state !== 'undefined') addressArr.push(address.state);
|
|
150
|
+
|
|
151
|
+
const addressStr = addressArr.filter((str) => !!str && str !== 'undefined').join(', ');
|
|
152
|
+
|
|
153
|
+
return address.zipCode && address.zipCode !== 'undefined'
|
|
154
|
+
? `${addressStr} ${address.zipCode}`
|
|
155
|
+
: addressStr;
|
|
80
156
|
}
|
|
81
157
|
|
|
82
158
|
export function databaseAddressStringToDisplayString(addressString: string | undefined | null): string {
|
|
83
|
-
if (addressString) {
|
|
84
|
-
|
|
85
|
-
if (!addressString.includes(ADDRESS_DELIM)) {
|
|
86
|
-
return addressString.trim();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const oneLineString = databaseAddressStringToOneLineString(addressString);
|
|
90
|
-
const formatted = oneLineString.replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ") // Add space between a single uppercase letter and a capitalized word
|
|
91
|
-
.replace(/(\d)(?=[A-Z])/g, "$1 ") // Add space between numbers and letters
|
|
92
|
-
.replace(/,/g, ", ") // Add space after commas
|
|
93
|
-
.replace(/undefined/g, ""); // Remove any "undefined" strings
|
|
94
|
-
return formatted;
|
|
159
|
+
if (!addressString) {
|
|
160
|
+
return '';
|
|
95
161
|
}
|
|
96
|
-
|
|
162
|
+
const oneLineString = databaseAddressStringToOneLineString(addressString);
|
|
163
|
+
const formatted = oneLineString
|
|
164
|
+
.replace(/([A-Z])(?=[A-Z][a-z])/g, '$1 ') // Add space between a single uppercase letter and a capitalized word
|
|
165
|
+
.replace(/(\d)(?=[A-Z])/g, '$1 ') // Add space between numbers and letters
|
|
166
|
+
.replace(/,/g, ', ') // Add space after commas
|
|
167
|
+
.replace(/undefined/g, ''); // Remove any "undefined" strings
|
|
168
|
+
return formatted;
|
|
97
169
|
}
|
|
98
170
|
|
|
99
171
|
export function addressToDisplayString(address: IAddress): string {
|
|
@@ -104,32 +176,76 @@ export function addressToDisplayString(address: IAddress): string {
|
|
|
104
176
|
}
|
|
105
177
|
|
|
106
178
|
|
|
179
|
+
/** Prefer a precise street-level result; `results[0]` is often a coarse area (wrong street line). */
|
|
180
|
+
function pickBestGeocodeResult(
|
|
181
|
+
results: Array<{
|
|
182
|
+
types?: string[];
|
|
183
|
+
geometry?: { location_type?: string };
|
|
184
|
+
address_components?: GoogleAddressComponent[];
|
|
185
|
+
formatted_address?: string;
|
|
186
|
+
}>
|
|
187
|
+
) {
|
|
188
|
+
if (!results?.length) return undefined;
|
|
189
|
+
const streetAddress = results.find((r) => r.types?.includes("street_address"));
|
|
190
|
+
if (streetAddress) return streetAddress;
|
|
191
|
+
const premise = results.find((r) => r.types?.includes("premise"));
|
|
192
|
+
if (premise) return premise;
|
|
193
|
+
const rooftop = results.find((r) => r.geometry?.location_type === "ROOFTOP");
|
|
194
|
+
if (rooftop) return rooftop;
|
|
195
|
+
const interpolated = results.find(
|
|
196
|
+
(r) => r.geometry?.location_type === "RANGE_INTERPOLATED"
|
|
197
|
+
);
|
|
198
|
+
if (interpolated) return interpolated;
|
|
199
|
+
return results[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
107
202
|
export async function getAddressFromCoordinates( lat: number, lng: number ): Promise<IAddress> {
|
|
108
203
|
const apiUrl = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${googleMapsApiKey}&loading=async`;
|
|
109
204
|
try {
|
|
110
205
|
const response = await fetch(apiUrl);
|
|
111
206
|
const data = await response.json();
|
|
112
207
|
if (data.results.length > 0) {
|
|
113
|
-
const
|
|
208
|
+
const firstResult = pickBestGeocodeResult(data.results);
|
|
209
|
+
if (!firstResult?.address_components) {
|
|
210
|
+
throw new Error("No address found");
|
|
211
|
+
}
|
|
212
|
+
const addressComponents = firstResult.address_components;
|
|
114
213
|
|
|
115
|
-
let
|
|
214
|
+
let streetNumber = "";
|
|
215
|
+
let route = "";
|
|
116
216
|
let city = "";
|
|
117
217
|
let state = "";
|
|
118
218
|
let zipCode = "";
|
|
119
219
|
|
|
120
220
|
addressComponents.forEach((component: GoogleAddressComponent) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
221
|
+
const types = component.types;
|
|
222
|
+
if (types.includes("street_number")) {
|
|
223
|
+
streetNumber = component.long_name;
|
|
224
|
+
}
|
|
225
|
+
if (types.includes("route")) {
|
|
226
|
+
route = component.long_name;
|
|
227
|
+
} else if (types.includes("locality")) {
|
|
124
228
|
city = component.long_name;
|
|
125
|
-
} else if (
|
|
229
|
+
} else if (types.includes("administrative_area_level_1")) {
|
|
126
230
|
state = component.short_name;
|
|
127
|
-
} else if (
|
|
231
|
+
} else if (types.includes("postal_code")) {
|
|
128
232
|
zipCode = component.long_name;
|
|
129
233
|
}
|
|
130
234
|
});
|
|
131
235
|
|
|
132
|
-
|
|
236
|
+
// Match extractAddressComponents: US addresses split street across street_number + route.
|
|
237
|
+
let street = `${streetNumber} ${route}`.trim();
|
|
238
|
+
if (!street && firstResult.formatted_address) {
|
|
239
|
+
const firstSeg = firstResult.formatted_address.split(",")[0]?.trim() ?? "";
|
|
240
|
+
if (firstSeg && city && !firstSeg.toLowerCase().includes(city.toLowerCase())) {
|
|
241
|
+
street = firstSeg;
|
|
242
|
+
} else if (firstSeg && !city) {
|
|
243
|
+
street = firstSeg;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Place is intentionally empty here — only filled when user picks a Place in Maps UI or types it.
|
|
248
|
+
return { place: "", street, city, state, zipCode, country: "USA" };
|
|
133
249
|
} else {
|
|
134
250
|
throw new Error("No address found");
|
|
135
251
|
}
|
|
@@ -645,11 +645,15 @@ describe("eligibilityValidator", () => {
|
|
|
645
645
|
const currentDate = new Date("2024-07-15T23:59:59Z");
|
|
646
646
|
const expiryDate = new Date("2024-07-16T00:00:01Z");
|
|
647
647
|
|
|
648
|
-
const offer = {
|
|
648
|
+
const offer = {
|
|
649
|
+
id: "offer-1",
|
|
650
|
+
isActive: true,
|
|
651
|
+
endDate: expiryDate,
|
|
652
|
+
} as any;
|
|
649
653
|
|
|
650
654
|
const result = validateOfferEligibility(offer, "user-1", 2, currentDate, 0);
|
|
651
655
|
|
|
652
|
-
expect(result.eligible).toBe(true); // Still valid by 2 seconds
|
|
656
|
+
expect(result.eligible).toBe(true); // Still valid by 2 seconds (UTC)
|
|
653
657
|
});
|
|
654
658
|
});
|
|
655
659
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared defaults for browser-side image compression (e.g. bash-app uploads).
|
|
3
|
+
* Env-specific values (max MB) stay in the app; dimensions/worker flags live here.
|
|
4
|
+
*/
|
|
5
|
+
export const CLIENT_IMAGE_COMPRESSION_MAX_WIDTH_OR_HEIGHT = 1920;
|
|
6
|
+
export const CLIENT_IMAGE_COMPRESSION_USE_WEB_WORKER = true;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract for Vendor / Exhibitor / Sponsor wizard “How” payment step (major 6, sub 3).
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_MAJOR_STEP,
|
|
6
|
+
PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_SUB_STEP,
|
|
7
|
+
partnerServiceProfileWizardPaymentMethodUrl,
|
|
8
|
+
} from "../serviceUtils";
|
|
9
|
+
|
|
10
|
+
describe("partnerServiceProfileWizardPaymentMethodUrl", () => {
|
|
11
|
+
it("uses How step 6 and sub-step 3", () => {
|
|
12
|
+
expect(PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_MAJOR_STEP).toBe(6);
|
|
13
|
+
expect(PARTNER_SERVICE_PROFILE_WIZARD_PAYMENT_METHOD_SUB_STEP).toBe(3);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
["Vendors", "s1", "p1"],
|
|
18
|
+
["Exhibitors", "s2", "p2"],
|
|
19
|
+
["Sponsors", "s3", "p3"],
|
|
20
|
+
] as const)("builds wizard URL for %s", (serviceType, serviceId, specificId) => {
|
|
21
|
+
expect(
|
|
22
|
+
partnerServiceProfileWizardPaymentMethodUrl(
|
|
23
|
+
serviceId,
|
|
24
|
+
serviceType,
|
|
25
|
+
specificId
|
|
26
|
+
)
|
|
27
|
+
).toBe(`/wz/services/${serviceId}/${serviceType}/${specificId}/6/3`);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ServiceCancellationPolicy } from "@prisma/client";
|
|
2
|
+
import {
|
|
3
|
+
SERVICE_CANCELLATION_POLICY_DATA,
|
|
4
|
+
type ServiceCancellationRefundPolicy,
|
|
5
|
+
} from "./serviceUtils.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a tier threshold from SERVICE_CANCELLATION_POLICY_DATA to hours so
|
|
9
|
+
* rules that mix `days` and `hours` are compared on one timeline.
|
|
10
|
+
*/
|
|
11
|
+
export function cancellationRuleThresholdHours(
|
|
12
|
+
rule: ServiceCancellationRefundPolicy
|
|
13
|
+
): number {
|
|
14
|
+
if (rule.days !== undefined) {
|
|
15
|
+
return rule.days * 24;
|
|
16
|
+
}
|
|
17
|
+
if (rule.hours !== undefined) {
|
|
18
|
+
return rule.hours;
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ResolveCancellationRefundOptions = {
|
|
24
|
+
/** Event start time (UTC or local — caller must be consistent). */
|
|
25
|
+
eventStart: Date;
|
|
26
|
+
/** Defaults to `new Date()`. */
|
|
27
|
+
now?: Date;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hours from `now` until `eventStart` (non-negative). Returns 0 if the event
|
|
32
|
+
* has started or `eventStart` is invalid.
|
|
33
|
+
*/
|
|
34
|
+
export function hoursUntilEventStart(
|
|
35
|
+
eventStart: Date,
|
|
36
|
+
now: Date = new Date()
|
|
37
|
+
): number {
|
|
38
|
+
const ms = eventStart.getTime() - now.getTime();
|
|
39
|
+
if (ms <= 0 || Number.isNaN(ms)) {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
return ms / (1000 * 60 * 60);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolves refund as a fraction in [0, 1] from {@link SERVICE_CANCELLATION_POLICY_DATA}.
|
|
47
|
+
* For each rule, if time until start is at least that rule's threshold, the rule
|
|
48
|
+
* applies; when multiple apply, the highest `refundPercentage` wins (matches
|
|
49
|
+
* tiered "full → partial → none" semantics).
|
|
50
|
+
*/
|
|
51
|
+
export function resolveCancellationRefundFraction(
|
|
52
|
+
policy: ServiceCancellationPolicy | null | undefined,
|
|
53
|
+
options: ResolveCancellationRefundOptions
|
|
54
|
+
): number {
|
|
55
|
+
if (policy == null || policy === ServiceCancellationPolicy.None) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const data = SERVICE_CANCELLATION_POLICY_DATA[policy];
|
|
59
|
+
if (!data?.refundPolicy?.length) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const now = options.now ?? new Date();
|
|
64
|
+
const hoursUntil = hoursUntilEventStart(options.eventStart, now);
|
|
65
|
+
if (hoursUntil <= 0) {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let bestPercent = 0;
|
|
70
|
+
for (const rule of data.refundPolicy) {
|
|
71
|
+
const thresholdHours = cancellationRuleThresholdHours(rule);
|
|
72
|
+
if (hoursUntil >= thresholdHours && rule.refundPercentage > bestPercent) {
|
|
73
|
+
bestPercent = rule.refundPercentage;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return bestPercent / 100;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The winning tier rule for display (e.g. service booking UI), or null if none.
|
|
82
|
+
*/
|
|
83
|
+
export function getMatchingCancellationRefundRule(
|
|
84
|
+
policy: ServiceCancellationPolicy | null | undefined,
|
|
85
|
+
options: ResolveCancellationRefundOptions
|
|
86
|
+
): ServiceCancellationRefundPolicy | null {
|
|
87
|
+
if (policy == null || policy === ServiceCancellationPolicy.None) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const data = SERVICE_CANCELLATION_POLICY_DATA[policy];
|
|
91
|
+
if (!data?.refundPolicy?.length) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const now = options.now ?? new Date();
|
|
96
|
+
const hoursUntil = hoursUntilEventStart(options.eventStart, now);
|
|
97
|
+
if (hoursUntil <= 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let bestRule: ServiceCancellationRefundPolicy | null = null;
|
|
102
|
+
let bestPercent = 0;
|
|
103
|
+
for (const rule of data.refundPolicy) {
|
|
104
|
+
const thresholdHours = cancellationRuleThresholdHours(rule);
|
|
105
|
+
if (hoursUntil >= thresholdHours && rule.refundPercentage > bestPercent) {
|
|
106
|
+
bestPercent = rule.refundPercentage;
|
|
107
|
+
bestRule = rule;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return bestRule;
|
|
112
|
+
}
|