@jarwizz/create-jarshop 0.1.4 → 0.1.6
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/template/src/email-smoke.ts +1 -0
- package/dist/template/src/migrations/1787800000000-jarshop-order-email-event-key.ts +60 -0
- package/dist/template/src/migrations/1787900000000-jarshop-email-brand-configuration.ts +32 -0
- package/dist/template/src/migrations/1788000000000-jarshop-email-brand-schema-parity.ts +21 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +213 -32
- package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +17 -0
- package/dist/template/src/plugins/jarshop-checkout/index.ts +49 -1
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +146 -5
- package/dist/template/src/plugins/jarshop-checkout/order-email-configuration.entity.ts +61 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-configuration.ts +190 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +321 -13
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +3 -1
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +15 -4
- package/dist/template/src/plugins/jarshop-checkout/order-email-lifecycle.ts +355 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +308 -15
- package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +86 -8
- package/dist/template/src/plugins/jarshop-checkout/order-lifecycle-core.ts +152 -0
- package/dist/template/src/plugins/jarshop-checkout/order-lifecycle-process.ts +102 -0
- package/dist/template-manifest.json +1 -1
- package/package.json +1 -1
|
@@ -33,18 +33,28 @@ import {
|
|
|
33
33
|
} from "./confirmation.js";
|
|
34
34
|
import { JarShopLegalConfiguration } from "./legal.entity.js";
|
|
35
35
|
import { JarShopOrderEmailDelivery } from "./order-email-delivery.entity.js";
|
|
36
|
+
import { JarShopEmailBrandConfiguration } from "./order-email-configuration.entity.js";
|
|
37
|
+
import {
|
|
38
|
+
normalizeOrderEmailBrandConfiguration,
|
|
39
|
+
toOrderEmailBrandConfiguration,
|
|
40
|
+
type OrderEmailBrandConfigurationInput,
|
|
41
|
+
} from "./order-email-configuration.js";
|
|
42
|
+
import { JarShopOrderEmailLifecycleOutbox } from "./order-email-lifecycle.js";
|
|
36
43
|
import {
|
|
37
44
|
isOrderAvailableInChannel,
|
|
38
45
|
toOrderEmailDeliveryStatus,
|
|
39
46
|
} from "./order-email-delivery-status.js";
|
|
40
47
|
import { createPendingOrderEmailDelivery } from "./order-email-outbox.js";
|
|
48
|
+
import { toOrderEmailLanguageCode } from "./order-email-delivery-core.js";
|
|
41
49
|
import {
|
|
42
50
|
JarShopOrderEmailWorker,
|
|
43
51
|
registerJarShopOrderEmailWorkerTask,
|
|
44
52
|
} from "./order-email-worker.js";
|
|
45
53
|
import { JarShopOrderConsent } from "./order-consent.entity.js";
|
|
54
|
+
import { registerJarShopOrderLifecycle } from "./order-lifecycle-process.js";
|
|
46
55
|
import {
|
|
47
56
|
compareCheckoutSnapshot,
|
|
57
|
+
resolveLocalizedShippingMethodName,
|
|
48
58
|
validateShippingSelection,
|
|
49
59
|
type CheckoutCartSnapshot,
|
|
50
60
|
type ShippingAddressInput,
|
|
@@ -162,12 +172,47 @@ const adminSchema = parse(`
|
|
|
162
172
|
sentAt: DateTime
|
|
163
173
|
lastErrorCode: String
|
|
164
174
|
}
|
|
175
|
+
type JarShopEmailShippingMethodConfiguration {
|
|
176
|
+
code: String!
|
|
177
|
+
pickupDetails: String!
|
|
178
|
+
trackingUrlTemplate: String!
|
|
179
|
+
}
|
|
180
|
+
input JarShopEmailShippingMethodConfigurationInput {
|
|
181
|
+
code: String!
|
|
182
|
+
pickupDetails: String!
|
|
183
|
+
trackingUrlTemplate: String!
|
|
184
|
+
}
|
|
185
|
+
type JarShopEmailBrandConfiguration {
|
|
186
|
+
brandName: String!
|
|
187
|
+
logoUrl: String
|
|
188
|
+
primaryColor: String!
|
|
189
|
+
websiteUrl: String!
|
|
190
|
+
supportEmail: String!
|
|
191
|
+
companyName: String!
|
|
192
|
+
companyAddress: String!
|
|
193
|
+
enabledEvents: [String!]!
|
|
194
|
+
shippingMethods: [JarShopEmailShippingMethodConfiguration!]!
|
|
195
|
+
}
|
|
196
|
+
input JarShopEmailBrandConfigurationInput {
|
|
197
|
+
brandName: String!
|
|
198
|
+
logoUrl: String
|
|
199
|
+
primaryColor: String!
|
|
200
|
+
websiteUrl: String!
|
|
201
|
+
supportEmail: String!
|
|
202
|
+
companyName: String!
|
|
203
|
+
companyAddress: String!
|
|
204
|
+
enabledEvents: [String!]!
|
|
205
|
+
shippingMethods: [JarShopEmailShippingMethodConfigurationInput!]!
|
|
206
|
+
}
|
|
165
207
|
extend type Query {
|
|
166
208
|
jarShopLegalConfiguration(languageCode: String!): JarShopLegalConfiguration
|
|
167
209
|
jarShopOrderEmailDeliveryStatus(orderId: ID!): JarShopOrderEmailDeliveryStatus
|
|
210
|
+
jarShopOrderEmailDeliveries(orderId: ID!): [JarShopOrderEmailDeliveryStatus!]!
|
|
211
|
+
jarShopEmailBrandConfiguration: JarShopEmailBrandConfiguration
|
|
168
212
|
}
|
|
169
213
|
extend type Mutation {
|
|
170
214
|
updateJarShopLegalConfiguration(input: JarShopLegalConfigurationInput!): JarShopLegalConfiguration!
|
|
215
|
+
updateJarShopEmailBrandConfiguration(input: JarShopEmailBrandConfigurationInput!): JarShopEmailBrandConfiguration!
|
|
171
216
|
}
|
|
172
217
|
`);
|
|
173
218
|
|
|
@@ -637,13 +682,51 @@ class JarShopCheckoutResolver {
|
|
|
637
682
|
ctx,
|
|
638
683
|
JarShopOrderEmailDelivery,
|
|
639
684
|
);
|
|
685
|
+
const orderEmailSnapshotSource = await this.orderService.findOne(
|
|
686
|
+
ctx,
|
|
687
|
+
paymentResult.id,
|
|
688
|
+
[
|
|
689
|
+
"customer",
|
|
690
|
+
"lines.productVariant",
|
|
691
|
+
"shippingLines.shippingMethod.translations",
|
|
692
|
+
"payments",
|
|
693
|
+
],
|
|
694
|
+
);
|
|
695
|
+
if (!orderEmailSnapshotSource?.customer?.emailAddress) {
|
|
696
|
+
throw new Error("order confirmation snapshot is incomplete");
|
|
697
|
+
}
|
|
698
|
+
const orderEmailLanguageCode = toOrderEmailLanguageCode(input.languageCode);
|
|
640
699
|
await emailDeliveryRepository.save(
|
|
641
700
|
emailDeliveryRepository.create(
|
|
642
701
|
createPendingOrderEmailDelivery({
|
|
643
702
|
orderId: Number(paymentResult.id),
|
|
644
703
|
checkoutAttemptId: attempt.attemptId,
|
|
645
|
-
|
|
646
|
-
|
|
704
|
+
snapshot: {
|
|
705
|
+
recipient: orderEmailSnapshotSource.customer.emailAddress,
|
|
706
|
+
languageCode: orderEmailLanguageCode,
|
|
707
|
+
orderCode: orderEmailSnapshotSource.code,
|
|
708
|
+
currencyCode: orderEmailSnapshotSource.currencyCode,
|
|
709
|
+
totalWithTax: orderEmailSnapshotSource.totalWithTax,
|
|
710
|
+
shippingWithTax: orderEmailSnapshotSource.shippingWithTax,
|
|
711
|
+
paymentMethod: orderEmailSnapshotSource.payments
|
|
712
|
+
.map((payment) => payment.method)
|
|
713
|
+
.join(", "),
|
|
714
|
+
shippingMethod: orderEmailSnapshotSource.shippingLines
|
|
715
|
+
.map((line) =>
|
|
716
|
+
resolveLocalizedShippingMethodName(
|
|
717
|
+
line.shippingMethod,
|
|
718
|
+
orderEmailLanguageCode,
|
|
719
|
+
),
|
|
720
|
+
)
|
|
721
|
+
.filter(Boolean)
|
|
722
|
+
.join(", "),
|
|
723
|
+
confirmationToken: confirmations.email.token,
|
|
724
|
+
lines: orderEmailSnapshotSource.lines.map((line) => ({
|
|
725
|
+
name: line.productVariant.name,
|
|
726
|
+
quantity: line.quantity,
|
|
727
|
+
totalWithTax: Math.round(line.proratedLinePriceWithTax),
|
|
728
|
+
})),
|
|
729
|
+
},
|
|
647
730
|
}),
|
|
648
731
|
),
|
|
649
732
|
);
|
|
@@ -728,10 +811,66 @@ class JarShopLegalAdminResolver {
|
|
|
728
811
|
|
|
729
812
|
const delivery = await this.connection
|
|
730
813
|
.getRepository(ctx, JarShopOrderEmailDelivery)
|
|
731
|
-
.findOne({
|
|
814
|
+
.findOne({
|
|
815
|
+
where: { orderId: parsedOrderId, kind: "order-confirmation" },
|
|
816
|
+
});
|
|
732
817
|
return delivery ? toOrderEmailDeliveryStatus(delivery) : null;
|
|
733
818
|
}
|
|
734
819
|
|
|
820
|
+
@Query()
|
|
821
|
+
@Allow(Permission.ReadOrder)
|
|
822
|
+
async jarShopOrderEmailDeliveries(
|
|
823
|
+
@Ctx() ctx: RequestContext,
|
|
824
|
+
@Args("orderId") orderId: string,
|
|
825
|
+
) {
|
|
826
|
+
const parsedOrderId = parsePositiveOrderId(orderId);
|
|
827
|
+
if (parsedOrderId === undefined) return [];
|
|
828
|
+
const order = await this.connection
|
|
829
|
+
.getRepository(ctx, Order)
|
|
830
|
+
.findOne({ where: { id: parsedOrderId }, relations: ["channels"] });
|
|
831
|
+
if (
|
|
832
|
+
!order ||
|
|
833
|
+
!isOrderAvailableInChannel(
|
|
834
|
+
order.channels.map((channel) => channel.id),
|
|
835
|
+
ctx.channelId,
|
|
836
|
+
)
|
|
837
|
+
) {
|
|
838
|
+
return [];
|
|
839
|
+
}
|
|
840
|
+
const deliveries = await this.connection
|
|
841
|
+
.getRepository(ctx, JarShopOrderEmailDelivery)
|
|
842
|
+
.find({ where: { orderId: parsedOrderId }, order: { createdAt: "ASC" } });
|
|
843
|
+
return deliveries.map(toOrderEmailDeliveryStatus);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
@Query()
|
|
847
|
+
@Allow(Permission.UpdateSettings)
|
|
848
|
+
async jarShopEmailBrandConfiguration(@Ctx() ctx: RequestContext) {
|
|
849
|
+
const configuration = await this.connection
|
|
850
|
+
.getRepository(ctx, JarShopEmailBrandConfiguration)
|
|
851
|
+
.findOne({ where: { channelId: Number(ctx.channelId) } });
|
|
852
|
+
return configuration ? toOrderEmailBrandConfiguration(configuration) : null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
@Mutation()
|
|
856
|
+
@Allow(Permission.UpdateSettings)
|
|
857
|
+
async updateJarShopEmailBrandConfiguration(
|
|
858
|
+
@Ctx() ctx: RequestContext,
|
|
859
|
+
@Args("input") input: OrderEmailBrandConfigurationInput,
|
|
860
|
+
) {
|
|
861
|
+
const normalized = normalizeOrderEmailBrandConfiguration(input);
|
|
862
|
+
const repository = this.connection.getRepository(
|
|
863
|
+
ctx,
|
|
864
|
+
JarShopEmailBrandConfiguration,
|
|
865
|
+
);
|
|
866
|
+
let configuration = await repository.findOne({
|
|
867
|
+
where: { channelId: Number(ctx.channelId) },
|
|
868
|
+
});
|
|
869
|
+
configuration ??= repository.create({ channelId: Number(ctx.channelId) });
|
|
870
|
+
Object.assign(configuration, normalized);
|
|
871
|
+
return toOrderEmailBrandConfiguration(await repository.save(configuration));
|
|
872
|
+
}
|
|
873
|
+
|
|
735
874
|
@Mutation()
|
|
736
875
|
@Allow(Permission.UpdateSettings)
|
|
737
876
|
async updateJarShopLegalConfiguration(
|
|
@@ -856,13 +995,15 @@ function toOrderConfirmation(order: Order) {
|
|
|
856
995
|
@VendurePlugin({
|
|
857
996
|
dashboard: "./dashboard/index.tsx",
|
|
858
997
|
imports: [PluginCommonModule],
|
|
859
|
-
providers: [JarShopOrderEmailWorker],
|
|
860
|
-
configuration:
|
|
998
|
+
providers: [JarShopOrderEmailLifecycleOutbox, JarShopOrderEmailWorker],
|
|
999
|
+
configuration: (config) =>
|
|
1000
|
+
registerJarShopOrderEmailWorkerTask(registerJarShopOrderLifecycle(config)),
|
|
861
1001
|
entities: [
|
|
862
1002
|
CheckoutAttempt,
|
|
863
1003
|
JarShopConfirmationToken,
|
|
864
1004
|
JarShopLegalConfiguration,
|
|
865
1005
|
JarShopOrderEmailDelivery,
|
|
1006
|
+
JarShopEmailBrandConfiguration,
|
|
866
1007
|
JarShopOrderConsent,
|
|
867
1008
|
],
|
|
868
1009
|
shopApiExtensions: {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
PrimaryGeneratedColumn,
|
|
6
|
+
Unique,
|
|
7
|
+
UpdateDateColumn,
|
|
8
|
+
} from "typeorm";
|
|
9
|
+
import type { FulfillmentLifecycleEmailKind } from "./order-email-outbox.js";
|
|
10
|
+
|
|
11
|
+
export type ConfigurableOrderEmailKind =
|
|
12
|
+
FulfillmentLifecycleEmailKind | "order-cancelled";
|
|
13
|
+
|
|
14
|
+
export type OrderEmailShippingMethodConfiguration = {
|
|
15
|
+
code: string;
|
|
16
|
+
pickupDetails: string;
|
|
17
|
+
trackingUrlTemplate: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@Entity({ name: "jarshop_email_brand_configuration" })
|
|
21
|
+
@Unique("UQ_jarshop_email_brand_configuration_channel", ["channelId"])
|
|
22
|
+
export class JarShopEmailBrandConfiguration {
|
|
23
|
+
@PrimaryGeneratedColumn()
|
|
24
|
+
id!: number;
|
|
25
|
+
|
|
26
|
+
@Column({ type: "int" })
|
|
27
|
+
channelId!: number;
|
|
28
|
+
|
|
29
|
+
@Column({ type: "varchar", length: 120 })
|
|
30
|
+
brandName!: string;
|
|
31
|
+
|
|
32
|
+
@Column({ type: "varchar", length: 2_048, nullable: true })
|
|
33
|
+
logoUrl!: string | null;
|
|
34
|
+
|
|
35
|
+
@Column({ type: "varchar", length: 7 })
|
|
36
|
+
primaryColor!: string;
|
|
37
|
+
|
|
38
|
+
@Column({ type: "varchar", length: 2_048 })
|
|
39
|
+
websiteUrl!: string;
|
|
40
|
+
|
|
41
|
+
@Column({ type: "varchar", length: 320 })
|
|
42
|
+
supportEmail!: string;
|
|
43
|
+
|
|
44
|
+
@Column({ type: "varchar", length: 160 })
|
|
45
|
+
companyName!: string;
|
|
46
|
+
|
|
47
|
+
@Column({ type: "text" })
|
|
48
|
+
companyAddress!: string;
|
|
49
|
+
|
|
50
|
+
@Column({ type: "jsonb", default: [] })
|
|
51
|
+
enabledEvents!: ConfigurableOrderEmailKind[];
|
|
52
|
+
|
|
53
|
+
@Column({ type: "jsonb", default: [] })
|
|
54
|
+
shippingMethods!: OrderEmailShippingMethodConfiguration[];
|
|
55
|
+
|
|
56
|
+
@CreateDateColumn()
|
|
57
|
+
createdAt!: Date;
|
|
58
|
+
|
|
59
|
+
@UpdateDateColumn()
|
|
60
|
+
updatedAt!: Date;
|
|
61
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigurableOrderEmailKind,
|
|
3
|
+
JarShopEmailBrandConfiguration,
|
|
4
|
+
OrderEmailShippingMethodConfiguration,
|
|
5
|
+
} from "./order-email-configuration.entity.js";
|
|
6
|
+
|
|
7
|
+
const configurableOrderEmailKinds = [
|
|
8
|
+
"pickup-ready",
|
|
9
|
+
"pickup-picked-up",
|
|
10
|
+
"shipment-shipped",
|
|
11
|
+
"shipment-delivered",
|
|
12
|
+
"order-cancelled",
|
|
13
|
+
] as const satisfies readonly ConfigurableOrderEmailKind[];
|
|
14
|
+
|
|
15
|
+
export type OrderEmailBrandConfigurationInput = {
|
|
16
|
+
brandName: string;
|
|
17
|
+
logoUrl?: string | null;
|
|
18
|
+
primaryColor: string;
|
|
19
|
+
websiteUrl: string;
|
|
20
|
+
supportEmail: string;
|
|
21
|
+
companyName: string;
|
|
22
|
+
companyAddress: string;
|
|
23
|
+
enabledEvents: string[];
|
|
24
|
+
shippingMethods: OrderEmailShippingMethodConfiguration[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function validateOrderEmailBrandConfiguration(
|
|
28
|
+
input: OrderEmailBrandConfigurationInput,
|
|
29
|
+
): void {
|
|
30
|
+
requireText(input.brandName, "brandName", 120);
|
|
31
|
+
requireText(input.companyName, "companyName", 160);
|
|
32
|
+
requireText(input.companyAddress, "companyAddress", 1_000);
|
|
33
|
+
requireEmail(input.supportEmail);
|
|
34
|
+
requireSafeUrl(input.websiteUrl, "websiteUrl");
|
|
35
|
+
if (input.logoUrl) requireSafeUrl(input.logoUrl, "logoUrl");
|
|
36
|
+
if (!/^#[0-9a-f]{6}$/iu.test(input.primaryColor)) {
|
|
37
|
+
throw new Error("primaryColor must use #RRGGBB");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const enabledEvents = new Set(input.enabledEvents);
|
|
41
|
+
if (
|
|
42
|
+
enabledEvents.size !== input.enabledEvents.length ||
|
|
43
|
+
[...enabledEvents].some(
|
|
44
|
+
(kind) => !configurableOrderEmailKinds.includes(kind as never),
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
throw new Error("enabledEvents contains an unsupported or duplicate kind");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const methodCodes = new Set<string>();
|
|
51
|
+
for (const method of input.shippingMethods) {
|
|
52
|
+
requireText(method.code, "shippingMethods.code", 64);
|
|
53
|
+
if (methodCodes.has(method.code)) {
|
|
54
|
+
throw new Error("shippingMethods contains a duplicate code");
|
|
55
|
+
}
|
|
56
|
+
methodCodes.add(method.code);
|
|
57
|
+
requireOptionalText(
|
|
58
|
+
method.pickupDetails,
|
|
59
|
+
"shippingMethods.pickupDetails",
|
|
60
|
+
1_000,
|
|
61
|
+
);
|
|
62
|
+
if (method.trackingUrlTemplate) {
|
|
63
|
+
validateTrackingUrlTemplate(method.trackingUrlTemplate);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
(enabledEvents.has("pickup-ready") ||
|
|
69
|
+
enabledEvents.has("pickup-picked-up")) &&
|
|
70
|
+
!input.shippingMethods.some(
|
|
71
|
+
(method) => method.code === "pickup" && method.pickupDetails.trim(),
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
throw new Error("pickup lifecycle email requires pickup method details");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeOrderEmailBrandConfiguration(
|
|
79
|
+
input: OrderEmailBrandConfigurationInput,
|
|
80
|
+
): OrderEmailBrandConfigurationInput {
|
|
81
|
+
validateOrderEmailBrandConfiguration(input);
|
|
82
|
+
return {
|
|
83
|
+
brandName: input.brandName.trim(),
|
|
84
|
+
logoUrl: input.logoUrl?.trim() || null,
|
|
85
|
+
primaryColor: input.primaryColor.toLowerCase(),
|
|
86
|
+
websiteUrl: input.websiteUrl.trim(),
|
|
87
|
+
supportEmail: input.supportEmail.trim().toLowerCase(),
|
|
88
|
+
companyName: input.companyName.trim(),
|
|
89
|
+
companyAddress: input.companyAddress.trim(),
|
|
90
|
+
enabledEvents: [...input.enabledEvents] as ConfigurableOrderEmailKind[],
|
|
91
|
+
shippingMethods: input.shippingMethods.map((method) => ({
|
|
92
|
+
code: method.code.trim(),
|
|
93
|
+
pickupDetails: method.pickupDetails.trim(),
|
|
94
|
+
trackingUrlTemplate: method.trackingUrlTemplate.trim(),
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isLifecycleEmailEnabled(
|
|
100
|
+
configuration: JarShopEmailBrandConfiguration | null | undefined,
|
|
101
|
+
kind: ConfigurableOrderEmailKind,
|
|
102
|
+
): configuration is JarShopEmailBrandConfiguration {
|
|
103
|
+
return configuration?.enabledEvents.includes(kind) ?? false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveShippingEmailConfiguration(
|
|
107
|
+
configuration: JarShopEmailBrandConfiguration,
|
|
108
|
+
code: string,
|
|
109
|
+
): OrderEmailShippingMethodConfiguration | undefined {
|
|
110
|
+
return configuration.shippingMethods.find((method) => method.code === code);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createTrackingUrl(
|
|
114
|
+
template: string,
|
|
115
|
+
trackingCode: string,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
if (!template || !trackingCode) return undefined;
|
|
118
|
+
validateTrackingUrlTemplate(template);
|
|
119
|
+
return template.replace(
|
|
120
|
+
"{trackingCode}",
|
|
121
|
+
encodeURIComponent(trackingCode.trim()),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function validateTrackingUrlTemplate(template: string): void {
|
|
126
|
+
if (
|
|
127
|
+
template.split("{trackingCode}").length !== 2 ||
|
|
128
|
+
/[\r\n]/u.test(template)
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"trackingUrlTemplate must contain one {trackingCode} placeholder",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
requireSafeUrl(
|
|
135
|
+
template.replace("{trackingCode}", "TRACKING"),
|
|
136
|
+
"trackingUrlTemplate",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function toOrderEmailBrandConfiguration(
|
|
141
|
+
configuration: JarShopEmailBrandConfiguration,
|
|
142
|
+
) {
|
|
143
|
+
return {
|
|
144
|
+
brandName: configuration.brandName,
|
|
145
|
+
logoUrl: configuration.logoUrl,
|
|
146
|
+
primaryColor: configuration.primaryColor,
|
|
147
|
+
websiteUrl: configuration.websiteUrl,
|
|
148
|
+
supportEmail: configuration.supportEmail,
|
|
149
|
+
companyName: configuration.companyName,
|
|
150
|
+
companyAddress: configuration.companyAddress,
|
|
151
|
+
enabledEvents: configuration.enabledEvents,
|
|
152
|
+
shippingMethods: configuration.shippingMethods,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function requireSafeUrl(value: string, field: string): void {
|
|
157
|
+
requireText(value, field, 2_048);
|
|
158
|
+
const url = new URL(value);
|
|
159
|
+
if (
|
|
160
|
+
url.protocol !== "https:" ||
|
|
161
|
+
url.username ||
|
|
162
|
+
url.password ||
|
|
163
|
+
url.origin === "null"
|
|
164
|
+
) {
|
|
165
|
+
throw new Error(`${field} must be an absolute credential-free HTTPS URL`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function requireEmail(value: string): void {
|
|
170
|
+
requireText(value, "supportEmail", 320);
|
|
171
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value)) {
|
|
172
|
+
throw new Error("supportEmail must be a valid email address");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function requireOptionalText(value: string, field: string, maximum: number) {
|
|
177
|
+
if (!value) return;
|
|
178
|
+
requireText(value, field, maximum);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function requireText(value: string, field: string, maximum: number): void {
|
|
182
|
+
const normalized = value.trim();
|
|
183
|
+
if (
|
|
184
|
+
!normalized ||
|
|
185
|
+
normalized.length > maximum ||
|
|
186
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(normalized)
|
|
187
|
+
) {
|
|
188
|
+
throw new Error(`${field} is invalid`);
|
|
189
|
+
}
|
|
190
|
+
}
|