@jarwizz/create-jarshop 0.1.1 → 0.1.3
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/README.md +10 -2
- package/dist/generator.js +10 -4
- package/dist/generator.js.map +1 -1
- package/dist/template/AGENTS.md +3 -3
- package/dist/template/CONTEXT.md +14 -0
- package/dist/template/README.md +25 -3
- package/dist/template/docker-compose.yml +5 -0
- package/dist/template/docs/smtp-smoke.md +37 -0
- package/dist/template/env.example.template +18 -0
- package/dist/template/gitignore.template +2 -0
- package/dist/template/package.json +19 -6
- package/dist/template/pnpm-lock.yaml +2389 -98
- package/dist/template/pnpm-workspace.yaml +12 -1
- package/dist/template/src/dev-storefront.ts +58 -0
- package/dist/template/src/dev.ts +67 -4
- package/dist/template/src/email-smoke.ts +27 -0
- package/dist/template/src/environment.d.ts +6 -0
- package/dist/template/src/migrate.ts +16 -0
- package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
- package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
- package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
- package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
- package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
- package/dist/template/src/plugins/jarshop-checkout/cod-payment.test.ts +44 -0
- package/dist/template/src/plugins/jarshop-checkout/cod-payment.ts +28 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +59 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
- package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
- package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
- package/dist/template/src/plugins/jarshop-checkout/index.ts +51 -0
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +870 -0
- package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
- package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
- package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
- package/dist/template/src/seed.ts +171 -0
- package/dist/template/src/vendure-config.ts +72 -7
- package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
- package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
- package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
- package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
- package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
- package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
- package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
- package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
- package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
- package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
- package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
- package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
- package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
- package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
- package/dist/template/storefront/app/[locale]/checkout/confirmation/route.ts +21 -0
- package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
- package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +28 -0
- package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
- package/dist/template/storefront/app/[locale]/page.tsx +12 -13
- package/dist/template/storefront/app/globals.css +372 -4
- package/dist/template/storefront/app/layout.tsx +13 -0
- package/dist/template/storefront/eslint.config.mjs +8 -1
- package/dist/template/storefront/gitignore.template +1 -0
- package/dist/template/storefront/lib/account-security.test.ts +74 -0
- package/dist/template/storefront/lib/account-security.ts +81 -0
- package/dist/template/storefront/lib/account.ts +366 -0
- package/dist/template/storefront/lib/cart-contract.ts +52 -0
- package/dist/template/storefront/lib/cart.test.ts +30 -0
- package/dist/template/storefront/lib/cart.ts +130 -0
- package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
- package/dist/template/storefront/lib/checkout-contract.ts +16 -0
- package/dist/template/storefront/lib/checkout.ts +149 -0
- package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
- package/dist/template/storefront/lib/commerce-session.ts +85 -0
- package/dist/template/storefront/lib/commerce.ts +129 -4
- package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
- package/dist/template/storefront/lib/confirmation.ts +95 -0
- package/dist/template/storefront/lib/form-values.test.ts +14 -0
- package/dist/template/storefront/lib/form-values.ts +7 -0
- package/dist/template/storefront/lib/i18n.test.ts +50 -0
- package/dist/template/storefront/lib/i18n.ts +44 -0
- package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
- package/dist/template/storefront/lib/password-reset-token.ts +17 -0
- package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
- package/dist/template/storefront/messages/en.json +89 -0
- package/dist/template/storefront/messages/sk.json +89 -0
- package/dist/template/storefront/next.config.ts +3 -1
- package/dist/template/storefront/package.json +11 -4
- package/dist/template/storefront/vitest.config.ts +8 -0
- package/dist/template/tsconfig.json +2 -1
- package/dist/template/turbo.json +8 -0
- package/dist/template/vite.config.mts +4 -1
- package/dist/template-manifest.json +1 -1
- package/dist/template.js +3 -0
- package/dist/template.js.map +1 -1
- package/package.json +1 -1
- package/dist/template/storefront/pnpm-lock.yaml +0 -3562
- package/dist/template/storefront/pnpm-workspace.yaml +0 -26
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
packages:
|
|
1
|
+
packages:
|
|
2
|
+
- .
|
|
3
|
+
- storefront
|
|
4
|
+
|
|
5
|
+
# The eslint-config-next range otherwise resolves a newer unreviewed resolver,
|
|
6
|
+
# which pnpm rejects as a trust downgrade. Keep the reviewed compatible
|
|
7
|
+
# resolver exact for the whole Client Project workspace.
|
|
8
|
+
overrides:
|
|
9
|
+
eslint-import-resolver-typescript: 3.8.7
|
|
10
|
+
|
|
11
|
+
minimumReleaseAgeExclude:
|
|
12
|
+
- "@jarwizz/commerce-sdk@0.3.0"
|
|
2
13
|
|
|
3
14
|
allowBuilds:
|
|
4
15
|
bcrypt: true
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
|
|
5
|
+
const root = resolve(__dirname, "..");
|
|
6
|
+
const inheritedEnvironment = { ...process.env };
|
|
7
|
+
|
|
8
|
+
function parsePort(value: string): number {
|
|
9
|
+
const port = Number(value);
|
|
10
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"JARSHOP_STOREFRONT_PORT must be an integer from 1 to 65535.",
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return port;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main(): Promise<void> {
|
|
19
|
+
dotenv.config({ path: resolve(root, ".env"), quiet: true });
|
|
20
|
+
const storefrontPort = parsePort(
|
|
21
|
+
process.env.JARSHOP_STOREFRONT_PORT ?? "3001",
|
|
22
|
+
);
|
|
23
|
+
const commerceEndpoint =
|
|
24
|
+
process.env.JARSHOP_COMMERCE_ENDPOINT ??
|
|
25
|
+
`http://localhost:${process.env.PORT ?? "3000"}/shop-api`;
|
|
26
|
+
const child = spawn(
|
|
27
|
+
"pnpm",
|
|
28
|
+
["--dir", "storefront", "dev", "--port", String(storefrontPort)],
|
|
29
|
+
{
|
|
30
|
+
cwd: root,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
// Values from the root .env are intentionally not forwarded to Next.
|
|
33
|
+
// Server, worker and database credentials therefore stay inside their
|
|
34
|
+
// own process; the local storefront receives only the commerce endpoint
|
|
35
|
+
// and its server-only local Valkey URL.
|
|
36
|
+
env: {
|
|
37
|
+
...inheritedEnvironment,
|
|
38
|
+
JARSHOP_COMMERCE_ENDPOINT: commerceEndpoint,
|
|
39
|
+
JARSHOP_ACCOUNT_RATE_LIMIT_URL:
|
|
40
|
+
process.env.JARSHOP_ACCOUNT_RATE_LIMIT_URL ??
|
|
41
|
+
"redis://localhost:6380",
|
|
42
|
+
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED ?? "1",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
await new Promise<void>((resolvePromise, reject) => {
|
|
47
|
+
child.once("error", reject);
|
|
48
|
+
child.once("exit", (code, signal) => {
|
|
49
|
+
if (code === 0) resolvePromise();
|
|
50
|
+
else reject(new Error(`storefront exited with ${signal ?? code}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch((error: unknown) => {
|
|
56
|
+
console.error(error instanceof Error ? error.message : error);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
});
|
package/dist/template/src/dev.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { copyFile,
|
|
1
|
+
import { access, appendFile, copyFile, readFile } from "node:fs/promises";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { createServer } from "node:net";
|
|
3
|
+
import { createConnection, createServer } from "node:net";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
6
6
|
import dotenv from "dotenv";
|
|
@@ -45,12 +45,59 @@ async function assertPortAvailable(
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
async function assertServiceReachable(
|
|
49
|
+
port: number,
|
|
50
|
+
service: string,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await new Promise<void>((resolvePromise, reject) => {
|
|
53
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
54
|
+
const cleanup = () => {
|
|
55
|
+
socket.removeAllListeners();
|
|
56
|
+
socket.destroy();
|
|
57
|
+
};
|
|
58
|
+
socket.once("connect", () => {
|
|
59
|
+
cleanup();
|
|
60
|
+
resolvePromise();
|
|
61
|
+
});
|
|
62
|
+
socket.once("error", (error) => {
|
|
63
|
+
cleanup();
|
|
64
|
+
reject(error);
|
|
65
|
+
});
|
|
66
|
+
socket.setTimeout(500, () => {
|
|
67
|
+
cleanup();
|
|
68
|
+
reject(new Error(`${service} is not reachable on localhost:${port}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
async function ensureEnv() {
|
|
49
74
|
try {
|
|
50
75
|
await access(envFile);
|
|
51
76
|
} catch {
|
|
52
77
|
await copyFile(envExample, envFile);
|
|
53
78
|
console.log("Created local .env from .env.example");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [currentEnvironment, exampleEnvironment] = await Promise.all([
|
|
83
|
+
readFile(envFile, "utf8"),
|
|
84
|
+
readFile(envExample, "utf8"),
|
|
85
|
+
]);
|
|
86
|
+
const currentKeys = new Set(
|
|
87
|
+
currentEnvironment
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.flatMap((line) => line.match(/^([A-Z][A-Z0-9_]*)=/)?.[1] ?? []),
|
|
90
|
+
);
|
|
91
|
+
const missingDefaults = exampleEnvironment.split(/\r?\n/).filter((line) => {
|
|
92
|
+
const key = line.match(/^([A-Z][A-Z0-9_]*)=/)?.[1];
|
|
93
|
+
return key !== undefined && !currentKeys.has(key);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (missingDefaults.length > 0) {
|
|
97
|
+
await appendFile(envFile, `\n${missingDefaults.join("\n")}\n`);
|
|
98
|
+
console.log(
|
|
99
|
+
`Added ${missingDefaults.length} missing local default${missingDefaults.length === 1 ? "" : "s"} from .env.example`,
|
|
100
|
+
);
|
|
54
101
|
}
|
|
55
102
|
}
|
|
56
103
|
|
|
@@ -77,6 +124,10 @@ async function dockerReady() {
|
|
|
77
124
|
process.env.JARSHOP_POSTGRES_PORT ?? process.env.DB_PORT ?? "6543",
|
|
78
125
|
"JARSHOP_POSTGRES_PORT",
|
|
79
126
|
);
|
|
127
|
+
const valkeyPort = parsePort(
|
|
128
|
+
process.env.JARSHOP_VALKEY_PORT ?? "6380",
|
|
129
|
+
"JARSHOP_VALKEY_PORT",
|
|
130
|
+
);
|
|
80
131
|
try {
|
|
81
132
|
await run("docker", ["info"], { stdio: "ignore" });
|
|
82
133
|
} catch {
|
|
@@ -86,7 +137,7 @@ async function dockerReady() {
|
|
|
86
137
|
}
|
|
87
138
|
|
|
88
139
|
try {
|
|
89
|
-
await run("docker", ["compose", "up", "-d", "postgres_db"]);
|
|
140
|
+
await run("docker", ["compose", "up", "-d", "postgres_db", "valkey"]);
|
|
90
141
|
} catch {
|
|
91
142
|
throw new Error(
|
|
92
143
|
`PostgreSQL could not start on port ${dbPort}. Stop the conflicting service or choose another JARSHOP_POSTGRES_PORT.`,
|
|
@@ -109,6 +160,13 @@ async function dockerReady() {
|
|
|
109
160
|
],
|
|
110
161
|
{ stdio: "ignore" },
|
|
111
162
|
);
|
|
163
|
+
// `pg_isready` proves readiness inside Docker. Docker Desktop can expose
|
|
164
|
+
// the container before its host port proxy is available, so also prove
|
|
165
|
+
// the connection that migrations use from the local machine.
|
|
166
|
+
await Promise.all([
|
|
167
|
+
assertServiceReachable(dbPort, "PostgreSQL"),
|
|
168
|
+
assertServiceReachable(valkeyPort, "Valkey"),
|
|
169
|
+
]);
|
|
112
170
|
return;
|
|
113
171
|
} catch {
|
|
114
172
|
await delay(1000);
|
|
@@ -124,9 +182,13 @@ async function main() {
|
|
|
124
182
|
dotenv.config({ path: envFile, quiet: true });
|
|
125
183
|
assertRuntime();
|
|
126
184
|
const serverPort = parsePort(process.env.PORT ?? "3000", "PORT");
|
|
185
|
+
const storefrontPort = parsePort(
|
|
186
|
+
process.env.JARSHOP_STOREFRONT_PORT ?? "3001",
|
|
187
|
+
"JARSHOP_STOREFRONT_PORT",
|
|
188
|
+
);
|
|
127
189
|
await Promise.all([
|
|
128
190
|
assertPortAvailable(serverPort, "Vendure"),
|
|
129
|
-
assertPortAvailable(
|
|
191
|
+
assertPortAvailable(storefrontPort, "storefront"),
|
|
130
192
|
]);
|
|
131
193
|
const isDev = (process.env.APP_ENV ?? "dev") === "dev";
|
|
132
194
|
if (isDev) {
|
|
@@ -141,6 +203,7 @@ async function main() {
|
|
|
141
203
|
"run",
|
|
142
204
|
"dev:server",
|
|
143
205
|
"dev:worker",
|
|
206
|
+
"dev:dashboard",
|
|
144
207
|
"dev:storefront",
|
|
145
208
|
`--ui=${turboUi}`,
|
|
146
209
|
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { parseSmtpSmokeCommand } from "./plugins/jarshop-checkout/email-smoke-core.js";
|
|
3
|
+
import { createConfiguredOrderEmailTransport } from "./plugins/jarshop-checkout/order-email-transport.js";
|
|
4
|
+
|
|
5
|
+
async function main(): Promise<void> {
|
|
6
|
+
const command = parseSmtpSmokeCommand(process.argv.slice(2), process.env);
|
|
7
|
+
const transport = createConfiguredOrderEmailTransport();
|
|
8
|
+
await transport.deliver({
|
|
9
|
+
deliveryId: 1,
|
|
10
|
+
email: {
|
|
11
|
+
recipient: command.recipient,
|
|
12
|
+
subject: "JarShop SMTP transport smoke",
|
|
13
|
+
text: [
|
|
14
|
+
"This is a controlled JarShop SMTP transport smoke message.",
|
|
15
|
+
"No customer order, bearer token, or checkout data is included.",
|
|
16
|
+
].join("\n"),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
console.log("SMTP smoke email was accepted by the configured transport.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch(() => {
|
|
23
|
+
console.error(
|
|
24
|
+
"SMTP smoke delivery failed. Check the provider configuration without copying credentials into logs.",
|
|
25
|
+
);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
});
|
|
@@ -5,6 +5,8 @@ declare global {
|
|
|
5
5
|
interface ProcessEnv {
|
|
6
6
|
APP_ENV?: string;
|
|
7
7
|
PORT?: string;
|
|
8
|
+
JARSHOP_STOREFRONT_PORT?: string;
|
|
9
|
+
JARSHOP_COMMERCE_ENDPOINT?: string;
|
|
8
10
|
COOKIE_SECRET?: string;
|
|
9
11
|
SUPERADMIN_USERNAME?: string;
|
|
10
12
|
SUPERADMIN_PASSWORD?: string;
|
|
@@ -14,6 +16,10 @@ declare global {
|
|
|
14
16
|
DB_USERNAME?: string;
|
|
15
17
|
DB_PASSWORD?: string;
|
|
16
18
|
DB_SCHEMA?: string;
|
|
19
|
+
JARSHOP_ORDER_EMAIL_ENCRYPTION_KEY?: string;
|
|
20
|
+
JARSHOP_EMAIL_TRANSPORT?: string;
|
|
21
|
+
JARSHOP_EMAIL_FILE_TRANSPORT_PATH?: string;
|
|
22
|
+
JARSHOP_STOREFRONT_URL?: string;
|
|
17
23
|
}
|
|
18
24
|
}
|
|
19
25
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { runMigrations } from "@vendure/core";
|
|
2
|
+
|
|
3
|
+
import { config } from "./vendure-config.js";
|
|
4
|
+
|
|
5
|
+
runMigrations(config)
|
|
6
|
+
.then((migrations) => {
|
|
7
|
+
if (migrations.length === 0) {
|
|
8
|
+
console.log("Migrations are up to date.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`Applied migrations: ${migrations.join(", ")}`);
|
|
12
|
+
})
|
|
13
|
+
.catch((error: unknown) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopCheckoutAttempt1786024108434 implements MigrationInterface {
|
|
4
|
+
name = "JarShopCheckoutAttempt1786024108434";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_checkout_attempt" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"attemptId" character varying(36) NOT NULL,
|
|
11
|
+
"marketCode" character varying(64) NOT NULL,
|
|
12
|
+
"languageCode" character varying(32) NOT NULL,
|
|
13
|
+
"status" character varying(16) NOT NULL,
|
|
14
|
+
"inputHash" character varying(128) NOT NULL,
|
|
15
|
+
"orderId" integer,
|
|
16
|
+
"errorCode" character varying(64),
|
|
17
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
18
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
19
|
+
CONSTRAINT "PK_jarshop_checkout_attempt" PRIMARY KEY ("id"),
|
|
20
|
+
CONSTRAINT "UQ_jarshop_checkout_attempt_attempt_id" UNIQUE ("attemptId")
|
|
21
|
+
)
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
26
|
+
await queryRunner.query(`DROP TABLE "jarshop_checkout_attempt"`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopLegalAndConfirmation1786024108435 implements MigrationInterface {
|
|
4
|
+
name = "JarShopLegalAndConfirmation1786024108435";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_legal_configuration" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"channelId" integer NOT NULL,
|
|
11
|
+
"languageCode" character varying(10) NOT NULL,
|
|
12
|
+
"termsVersion" character varying(64) NOT NULL,
|
|
13
|
+
"termsUrl" character varying(2048) NOT NULL,
|
|
14
|
+
"privacyVersion" character varying(64) NOT NULL,
|
|
15
|
+
"privacyUrl" character varying(2048) NOT NULL,
|
|
16
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
17
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
18
|
+
CONSTRAINT "PK_jarshop_legal_configuration" PRIMARY KEY ("id"),
|
|
19
|
+
CONSTRAINT "UQ_jarshop_legal_channel_language" UNIQUE ("channelId", "languageCode")
|
|
20
|
+
)
|
|
21
|
+
`);
|
|
22
|
+
await queryRunner.query(`
|
|
23
|
+
CREATE TABLE "jarshop_order_consent" (
|
|
24
|
+
"id" SERIAL NOT NULL,
|
|
25
|
+
"orderId" integer NOT NULL,
|
|
26
|
+
"termsVersion" character varying(64) NOT NULL,
|
|
27
|
+
"termsUrl" character varying(2048) NOT NULL,
|
|
28
|
+
"privacyUrl" character varying(2048) NOT NULL,
|
|
29
|
+
"acceptedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
30
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
31
|
+
CONSTRAINT "PK_jarshop_order_consent" PRIMARY KEY ("id"),
|
|
32
|
+
CONSTRAINT "UQ_jarshop_order_consent_order" UNIQUE ("orderId")
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
await queryRunner.query(`
|
|
36
|
+
CREATE TABLE "jarshop_confirmation_token" (
|
|
37
|
+
"id" SERIAL NOT NULL,
|
|
38
|
+
"tokenHash" character varying(128) NOT NULL,
|
|
39
|
+
"orderId" integer NOT NULL,
|
|
40
|
+
"expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
41
|
+
"consumedAt" TIMESTAMP WITH TIME ZONE,
|
|
42
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
43
|
+
CONSTRAINT "PK_jarshop_confirmation_token" PRIMARY KEY ("id"),
|
|
44
|
+
CONSTRAINT "UQ_jarshop_confirmation_token_hash" UNIQUE ("tokenHash")
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
await queryRunner.query(
|
|
48
|
+
`CREATE INDEX "IDX_jarshop_confirmation_token_order" ON "jarshop_confirmation_token" ("orderId")`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
53
|
+
await queryRunner.query(
|
|
54
|
+
`DROP INDEX "IDX_jarshop_confirmation_token_order"`,
|
|
55
|
+
);
|
|
56
|
+
await queryRunner.query(`DROP TABLE "jarshop_confirmation_token"`);
|
|
57
|
+
await queryRunner.query(`DROP TABLE "jarshop_order_consent"`);
|
|
58
|
+
await queryRunner.query(`DROP TABLE "jarshop_legal_configuration"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class JarShopOrderEmailOutbox1786024108436 implements MigrationInterface {
|
|
4
|
+
name = "JarShopOrderEmailOutbox1786024108436";
|
|
5
|
+
|
|
6
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`
|
|
8
|
+
CREATE TABLE "jarshop_order_email_delivery" (
|
|
9
|
+
"id" SERIAL NOT NULL,
|
|
10
|
+
"orderId" integer NOT NULL,
|
|
11
|
+
"checkoutAttemptId" character varying(36) NOT NULL,
|
|
12
|
+
"kind" character varying(64) NOT NULL,
|
|
13
|
+
"languageCode" character varying(10) NOT NULL,
|
|
14
|
+
"status" character varying(16) NOT NULL,
|
|
15
|
+
"payloadCiphertext" text NOT NULL,
|
|
16
|
+
"attemptCount" integer NOT NULL DEFAULT 0,
|
|
17
|
+
"nextAttemptAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
18
|
+
"lastErrorCode" character varying(64),
|
|
19
|
+
"sentAt" TIMESTAMP WITH TIME ZONE,
|
|
20
|
+
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
21
|
+
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
|
|
22
|
+
CONSTRAINT "PK_jarshop_order_email_delivery" PRIMARY KEY ("id"),
|
|
23
|
+
CONSTRAINT "UQ_jarshop_order_email_delivery_order_kind" UNIQUE ("orderId", "kind")
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
await queryRunner.query(
|
|
27
|
+
`CREATE INDEX "IDX_jarshop_order_email_delivery_pending" ON "jarshop_order_email_delivery" ("status", "nextAttemptAt")`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
32
|
+
await queryRunner.query(
|
|
33
|
+
`DROP INDEX "IDX_jarshop_order_email_delivery_pending"`,
|
|
34
|
+
);
|
|
35
|
+
await queryRunner.query(`DROP TABLE "jarshop_order_email_delivery"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is additive and nullable: existing pending/sent deliveries remain valid
|
|
5
|
+
* while application code rolls forward. It must not delete delivery intent.
|
|
6
|
+
*/
|
|
7
|
+
export class JarShopOrderEmailDeliveryLease1786030000000 implements MigrationInterface {
|
|
8
|
+
name = "JarShopOrderEmailDeliveryLease1786030000000";
|
|
9
|
+
|
|
10
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
11
|
+
await queryRunner.query(
|
|
12
|
+
`ALTER TABLE "jarshop_order_email_delivery" ADD "leaseToken" character varying(36)`,
|
|
13
|
+
);
|
|
14
|
+
await queryRunner.query(
|
|
15
|
+
`ALTER TABLE "jarshop_order_email_delivery" ADD "leaseExpiresAt" TIMESTAMP WITH TIME ZONE`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
20
|
+
await queryRunner.query(
|
|
21
|
+
`ALTER TABLE "jarshop_order_email_delivery" DROP COLUMN "leaseExpiresAt"`,
|
|
22
|
+
);
|
|
23
|
+
await queryRunner.query(
|
|
24
|
+
`ALTER TABLE "jarshop_order_email_delivery" DROP COLUMN "leaseToken"`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
PrimaryGeneratedColumn,
|
|
6
|
+
UpdateDateColumn,
|
|
7
|
+
} from "typeorm";
|
|
8
|
+
|
|
9
|
+
export type CheckoutAttemptStatus = "processing" | "completed" | "failed";
|
|
10
|
+
|
|
11
|
+
@Entity({ name: "jarshop_checkout_attempt" })
|
|
12
|
+
export class CheckoutAttempt {
|
|
13
|
+
@PrimaryGeneratedColumn()
|
|
14
|
+
id!: number;
|
|
15
|
+
|
|
16
|
+
@Column({ type: "varchar", length: 36, unique: true })
|
|
17
|
+
attemptId!: string;
|
|
18
|
+
|
|
19
|
+
@Column({ type: "varchar", length: 64 })
|
|
20
|
+
marketCode!: string;
|
|
21
|
+
|
|
22
|
+
@Column({ type: "varchar", length: 32 })
|
|
23
|
+
languageCode!: string;
|
|
24
|
+
|
|
25
|
+
@Column({ type: "varchar", length: 16 })
|
|
26
|
+
status!: CheckoutAttemptStatus;
|
|
27
|
+
|
|
28
|
+
@Column({ type: "varchar", length: 128 })
|
|
29
|
+
inputHash!: string;
|
|
30
|
+
|
|
31
|
+
@Column({ type: "int", nullable: true })
|
|
32
|
+
orderId!: number | null;
|
|
33
|
+
|
|
34
|
+
@Column({ type: "varchar", length: 64, nullable: true })
|
|
35
|
+
errorCode!: string | null;
|
|
36
|
+
|
|
37
|
+
@CreateDateColumn()
|
|
38
|
+
createdAt!: Date;
|
|
39
|
+
|
|
40
|
+
@UpdateDateColumn()
|
|
41
|
+
updatedAt!: Date;
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
Order,
|
|
4
|
+
Payment,
|
|
5
|
+
PaymentMethod,
|
|
6
|
+
RequestContext,
|
|
7
|
+
} from "@vendure/core";
|
|
8
|
+
import { jarShopCodPaymentHandler } from "./cod-payment.js";
|
|
9
|
+
|
|
10
|
+
const context = {} as RequestContext;
|
|
11
|
+
const order = { code: "ORDER-123" } as Order;
|
|
12
|
+
const method = {} as PaymentMethod;
|
|
13
|
+
|
|
14
|
+
describe("jarShopCodPaymentHandler", () => {
|
|
15
|
+
it("authorizes COD without pretending that money was collected", async () => {
|
|
16
|
+
const result = await jarShopCodPaymentHandler.createPayment(
|
|
17
|
+
context,
|
|
18
|
+
order,
|
|
19
|
+
12_345,
|
|
20
|
+
[],
|
|
21
|
+
{ attemptId: "attempt-1" },
|
|
22
|
+
method,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(result).toMatchObject({
|
|
26
|
+
amount: 12_345,
|
|
27
|
+
state: "Authorized",
|
|
28
|
+
transactionId: "cod:ORDER-123",
|
|
29
|
+
method: "jarshop-cod-handler",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("allows an operator-driven settlement after collection", async () => {
|
|
34
|
+
const result = await jarShopCodPaymentHandler.settlePayment(
|
|
35
|
+
context,
|
|
36
|
+
order,
|
|
37
|
+
{} as Payment,
|
|
38
|
+
[],
|
|
39
|
+
method,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(result).toEqual({ success: true });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LanguageCode, PaymentMethodHandler } from "@vendure/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cash on delivery is a two-step payment: checkout authorizes collection, and
|
|
5
|
+
* an authenticated operator settles it only after money is actually received.
|
|
6
|
+
*/
|
|
7
|
+
export const jarShopCodPaymentHandler = new PaymentMethodHandler({
|
|
8
|
+
code: "jarshop-cod-handler",
|
|
9
|
+
description: [
|
|
10
|
+
{
|
|
11
|
+
languageCode: LanguageCode.sk,
|
|
12
|
+
value: "Dobierka s manuálnym potvrdením prijatia platby",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
languageCode: LanguageCode.en,
|
|
16
|
+
value: "Cash on delivery with manual payment collection",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
args: {},
|
|
20
|
+
createPayment: async (_ctx, order, amount, _args, metadata) => ({
|
|
21
|
+
amount,
|
|
22
|
+
state: "Authorized" as const,
|
|
23
|
+
transactionId: `cod:${order.code}`,
|
|
24
|
+
metadata,
|
|
25
|
+
}),
|
|
26
|
+
settlePayment: async () => ({ success: true }),
|
|
27
|
+
cancelPayment: async () => ({ success: true }),
|
|
28
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
Index,
|
|
6
|
+
PrimaryGeneratedColumn,
|
|
7
|
+
Unique,
|
|
8
|
+
} from "typeorm";
|
|
9
|
+
|
|
10
|
+
@Entity({ name: "jarshop_confirmation_token" })
|
|
11
|
+
export class JarShopConfirmationToken {
|
|
12
|
+
@PrimaryGeneratedColumn()
|
|
13
|
+
id!: number;
|
|
14
|
+
|
|
15
|
+
@Unique("UQ_jarshop_confirmation_token_hash", ["tokenHash"])
|
|
16
|
+
@Column({ type: "varchar", length: 128 })
|
|
17
|
+
tokenHash!: string;
|
|
18
|
+
|
|
19
|
+
@Index("IDX_jarshop_confirmation_token_order")
|
|
20
|
+
@Column({ type: "int" })
|
|
21
|
+
orderId!: number;
|
|
22
|
+
|
|
23
|
+
@Column({ type: "timestamptz" })
|
|
24
|
+
expiresAt!: Date;
|
|
25
|
+
|
|
26
|
+
@Column({ type: "timestamptz", nullable: true })
|
|
27
|
+
consumedAt!: Date | null;
|
|
28
|
+
|
|
29
|
+
@CreateDateColumn()
|
|
30
|
+
createdAt!: Date;
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const CONFIRMATION_TTL_MS = 2 * 60 * 60 * 1000;
|
|
4
|
+
export const CONFIRMATION_COOKIE_NAME = "jarshop-confirmation";
|
|
5
|
+
|
|
6
|
+
export function hashConfirmationToken(token: string): string {
|
|
7
|
+
return createHash("sha256").update(token).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createConfirmationToken(now = Date.now()) {
|
|
11
|
+
const token = randomBytes(32).toString("base64url");
|
|
12
|
+
return {
|
|
13
|
+
token,
|
|
14
|
+
tokenHash: hashConfirmationToken(token),
|
|
15
|
+
expiresAt: new Date(now + CONFIRMATION_TTL_MS),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function canExchangeConfirmationToken(input: {
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
consumedAt: Date | null;
|
|
22
|
+
now?: number;
|
|
23
|
+
}): boolean {
|
|
24
|
+
return (
|
|
25
|
+
input.consumedAt === null &&
|
|
26
|
+
input.expiresAt.getTime() > (input.now ?? Date.now())
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function canReadConfirmationCredential(input: {
|
|
31
|
+
expiresAt: Date;
|
|
32
|
+
consumedAt: Date | null;
|
|
33
|
+
now?: number;
|
|
34
|
+
}): boolean {
|
|
35
|
+
return (
|
|
36
|
+
input.consumedAt !== null &&
|
|
37
|
+
input.expiresAt.getTime() > (input.now ?? Date.now())
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function readCookieValue(
|
|
42
|
+
cookieHeader: string,
|
|
43
|
+
cookieName: string,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
for (const part of cookieHeader.split(";")) {
|
|
46
|
+
const separator = part.indexOf("=");
|
|
47
|
+
if (separator < 0 || part.slice(0, separator).trim() !== cookieName) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const value = part.slice(separator + 1).trim();
|
|
51
|
+
if (!value) return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return decodeURIComponent(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|