@jarwizz/create-jarshop 0.1.3 → 0.1.5
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/plugins/jarshop-checkout/confirmation.ts +13 -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 +160 -12
- 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 +335 -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 +131 -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
|
@@ -27,23 +27,34 @@ import {
|
|
|
27
27
|
canExchangeConfirmationToken,
|
|
28
28
|
canReadConfirmationCredential,
|
|
29
29
|
createConfirmationToken,
|
|
30
|
+
createConfirmationTokenPair,
|
|
30
31
|
hashConfirmationToken,
|
|
31
32
|
readCookieValue,
|
|
32
33
|
} from "./confirmation.js";
|
|
33
34
|
import { JarShopLegalConfiguration } from "./legal.entity.js";
|
|
34
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";
|
|
35
43
|
import {
|
|
36
44
|
isOrderAvailableInChannel,
|
|
37
45
|
toOrderEmailDeliveryStatus,
|
|
38
46
|
} from "./order-email-delivery-status.js";
|
|
39
47
|
import { createPendingOrderEmailDelivery } from "./order-email-outbox.js";
|
|
48
|
+
import { toOrderEmailLanguageCode } from "./order-email-delivery-core.js";
|
|
40
49
|
import {
|
|
41
50
|
JarShopOrderEmailWorker,
|
|
42
51
|
registerJarShopOrderEmailWorkerTask,
|
|
43
52
|
} from "./order-email-worker.js";
|
|
44
53
|
import { JarShopOrderConsent } from "./order-consent.entity.js";
|
|
54
|
+
import { registerJarShopOrderLifecycle } from "./order-lifecycle-process.js";
|
|
45
55
|
import {
|
|
46
56
|
compareCheckoutSnapshot,
|
|
57
|
+
resolveLocalizedShippingMethodName,
|
|
47
58
|
validateShippingSelection,
|
|
48
59
|
type CheckoutCartSnapshot,
|
|
49
60
|
type ShippingAddressInput,
|
|
@@ -161,12 +172,47 @@ const adminSchema = parse(`
|
|
|
161
172
|
sentAt: DateTime
|
|
162
173
|
lastErrorCode: String
|
|
163
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
|
+
}
|
|
164
207
|
extend type Query {
|
|
165
208
|
jarShopLegalConfiguration(languageCode: String!): JarShopLegalConfiguration
|
|
166
209
|
jarShopOrderEmailDeliveryStatus(orderId: ID!): JarShopOrderEmailDeliveryStatus
|
|
210
|
+
jarShopOrderEmailDeliveries(orderId: ID!): [JarShopOrderEmailDeliveryStatus!]!
|
|
211
|
+
jarShopEmailBrandConfiguration: JarShopEmailBrandConfiguration
|
|
167
212
|
}
|
|
168
213
|
extend type Mutation {
|
|
169
214
|
updateJarShopLegalConfiguration(input: JarShopLegalConfigurationInput!): JarShopLegalConfiguration!
|
|
215
|
+
updateJarShopEmailBrandConfiguration(input: JarShopEmailBrandConfigurationInput!): JarShopEmailBrandConfiguration!
|
|
170
216
|
}
|
|
171
217
|
`);
|
|
172
218
|
|
|
@@ -613,30 +659,74 @@ class JarShopCheckoutResolver {
|
|
|
613
659
|
acceptedAt: new Date(),
|
|
614
660
|
}),
|
|
615
661
|
);
|
|
616
|
-
const
|
|
662
|
+
const confirmations = createConfirmationTokenPair();
|
|
617
663
|
const tokenRepository = this.connection.getRepository(
|
|
618
664
|
ctx,
|
|
619
665
|
JarShopConfirmationToken,
|
|
620
666
|
);
|
|
621
|
-
await tokenRepository.save(
|
|
667
|
+
await tokenRepository.save([
|
|
622
668
|
tokenRepository.create({
|
|
623
|
-
tokenHash:
|
|
669
|
+
tokenHash: confirmations.browser.tokenHash,
|
|
624
670
|
orderId: Number(paymentResult.id),
|
|
625
|
-
expiresAt:
|
|
671
|
+
expiresAt: confirmations.browser.expiresAt,
|
|
626
672
|
consumedAt: null,
|
|
627
673
|
}),
|
|
628
|
-
|
|
674
|
+
tokenRepository.create({
|
|
675
|
+
tokenHash: confirmations.email.tokenHash,
|
|
676
|
+
orderId: Number(paymentResult.id),
|
|
677
|
+
expiresAt: confirmations.email.expiresAt,
|
|
678
|
+
consumedAt: null,
|
|
679
|
+
}),
|
|
680
|
+
]);
|
|
629
681
|
const emailDeliveryRepository = this.connection.getRepository(
|
|
630
682
|
ctx,
|
|
631
683
|
JarShopOrderEmailDelivery,
|
|
632
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);
|
|
633
699
|
await emailDeliveryRepository.save(
|
|
634
700
|
emailDeliveryRepository.create(
|
|
635
701
|
createPendingOrderEmailDelivery({
|
|
636
702
|
orderId: Number(paymentResult.id),
|
|
637
703
|
checkoutAttemptId: attempt.attemptId,
|
|
638
|
-
|
|
639
|
-
|
|
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
|
+
},
|
|
640
730
|
}),
|
|
641
731
|
),
|
|
642
732
|
);
|
|
@@ -650,13 +740,13 @@ class JarShopCheckoutResolver {
|
|
|
650
740
|
placedOrder,
|
|
651
741
|
activeOrder.totalWithTax,
|
|
652
742
|
activeOrder.currencyCode,
|
|
653
|
-
|
|
743
|
+
confirmations.browser.token,
|
|
654
744
|
)
|
|
655
745
|
: success(
|
|
656
746
|
paymentResult,
|
|
657
747
|
activeOrder.totalWithTax,
|
|
658
748
|
activeOrder.currencyCode,
|
|
659
|
-
|
|
749
|
+
confirmations.browser.token,
|
|
660
750
|
);
|
|
661
751
|
}
|
|
662
752
|
|
|
@@ -721,10 +811,66 @@ class JarShopLegalAdminResolver {
|
|
|
721
811
|
|
|
722
812
|
const delivery = await this.connection
|
|
723
813
|
.getRepository(ctx, JarShopOrderEmailDelivery)
|
|
724
|
-
.findOne({
|
|
814
|
+
.findOne({
|
|
815
|
+
where: { orderId: parsedOrderId, kind: "order-confirmation" },
|
|
816
|
+
});
|
|
725
817
|
return delivery ? toOrderEmailDeliveryStatus(delivery) : null;
|
|
726
818
|
}
|
|
727
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
|
+
|
|
728
874
|
@Mutation()
|
|
729
875
|
@Allow(Permission.UpdateSettings)
|
|
730
876
|
async updateJarShopLegalConfiguration(
|
|
@@ -849,13 +995,15 @@ function toOrderConfirmation(order: Order) {
|
|
|
849
995
|
@VendurePlugin({
|
|
850
996
|
dashboard: "./dashboard/index.tsx",
|
|
851
997
|
imports: [PluginCommonModule],
|
|
852
|
-
providers: [JarShopOrderEmailWorker],
|
|
853
|
-
configuration:
|
|
998
|
+
providers: [JarShopOrderEmailLifecycleOutbox, JarShopOrderEmailWorker],
|
|
999
|
+
configuration: (config) =>
|
|
1000
|
+
registerJarShopOrderEmailWorkerTask(registerJarShopOrderLifecycle(config)),
|
|
854
1001
|
entities: [
|
|
855
1002
|
CheckoutAttempt,
|
|
856
1003
|
JarShopConfirmationToken,
|
|
857
1004
|
JarShopLegalConfiguration,
|
|
858
1005
|
JarShopOrderEmailDelivery,
|
|
1006
|
+
JarShopEmailBrandConfiguration,
|
|
859
1007
|
JarShopOrderConsent,
|
|
860
1008
|
],
|
|
861
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: () => "'[]'::jsonb" })
|
|
51
|
+
enabledEvents!: ConfigurableOrderEmailKind[];
|
|
52
|
+
|
|
53
|
+
@Column({ type: "jsonb", default: () => "'[]'::jsonb" })
|
|
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
|
+
}
|