@huloglobal/vendure-plugin-visitor-analytics 0.17.1 → 0.18.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/CHANGELOG.md +21 -0
- package/README.md +80 -4
- package/dist/abandoned-cart.controller.d.ts +70 -14
- package/dist/abandoned-cart.controller.d.ts.map +1 -1
- package/dist/abandoned-cart.controller.js +214 -13
- package/dist/abandoned-cart.controller.js.map +1 -1
- package/dist/abandoned-cart.entity.d.ts +15 -0
- package/dist/abandoned-cart.entity.d.ts.map +1 -1
- package/dist/abandoned-cart.entity.js +20 -0
- package/dist/abandoned-cart.entity.js.map +1 -1
- package/dist/abandoned-cart.service.d.ts +133 -7
- package/dist/abandoned-cart.service.d.ts.map +1 -1
- package/dist/abandoned-cart.service.js +286 -10
- package/dist/abandoned-cart.service.js.map +1 -1
- package/dist/hulo-storefront.d.ts.map +1 -1
- package/dist/hulo-storefront.js +52 -0
- package/dist/hulo-storefront.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/recovery-tokens.d.ts +56 -0
- package/dist/recovery-tokens.d.ts.map +1 -0
- package/dist/recovery-tokens.js +125 -0
- package/dist/recovery-tokens.js.map +1 -0
- package/dist/recovery-tokens.test.d.ts +2 -0
- package/dist/recovery-tokens.test.d.ts.map +1 -0
- package/dist/recovery-tokens.test.js +115 -0
- package/dist/recovery-tokens.test.js.map +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ documented here. The format follows
|
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project
|
|
6
6
|
adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.18.0] — 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Recovery links can resume an order.** `issueRecoveryLink(cartId, { resumeOrderCode })` (and `POST /ees/abandoned-carts/:id/recovery-link` with the same body) binds the link to the visitor's open Vendure order. `GET /ees/recover-cart` now returns `orderCode`, `orderState` and `resumable`, and the new public `POST /ees/recover-cart/resume?t=…` adds `resumeOrderCode` — set only while that order is still `AddingItems` / `ArrangingPayment`. Storefronts keep re-adding `items` as the universal fallback.
|
|
12
|
+
- **Attribution.** New `abandoned_cart` columns `recoveryStep` (`link_issued` → `link_opened` → `resumed` → `converted`, monotonic), `convertedAt`, `convertedOrderId`, `convertedOrderCode` and `resumeOrderCode`. The storefront reports a recovered checkout with the token-bound public `POST /ees/recover-cart/converted { t, orderCode }`; the scanner's own `checkout_completed` match sets `convertedAt` too. `GET /ees/abandoned-carts/summary` gains an `attribution` block (link issued / opened / resumed / converted counts, value recovered via link, opt-outs) and the list endpoint returns the new columns.
|
|
13
|
+
- **Email opt-out.** New `abandoned_cart_opt_out` table. `GET|POST /ees/abandoned-carts/opt-out?e=<token>` (public; POST is the RFC 8058 one-click form) records an opt-out for the address in the HMAC token. Service API: `isOptedOut(email)` (fails closed), `optOut`, `optIn`, `listOptOuts`, `buildOptOutLink(email)` and `buildListUnsubscribeHeaders(email)` for the `List-Unsubscribe` / `List-Unsubscribe-Post` headers. Admin: `GET /ees/abandoned-carts/opt-outs`, `POST /ees/abandoned-carts/opt-outs/remove { email }`; the detail endpoint reports `optedOut`. New option `abandonment.optOutSecret` (defaults to `recoveryLinkSecret`, then `signingSecret`).
|
|
14
|
+
- **Storefront helper.** `hulo.resumeCart(token)` and `hulo.recoveryConverted(orderCode)` — the helper remembers the token from `restoreCart` / `resumeCart` in `sessionStorage`, so the thank-you page needs one call.
|
|
15
|
+
- Pure helpers exported for hosts: `isResumableOrderState`, `advanceRecoveryStep`, `normaliseEmail`, `hashEmail`, `buildOptOutToken`, `verifyOptOutToken`, `buildOptOutUrl`, `buildListUnsubscribeHeaders`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- The public recovery endpoints are rate-limited per client IP (`recover-cart` and `resume` 30/min, `converted` and `opt-out` 10/min) and answer with `Cache-Control: no-store`.
|
|
19
|
+
- The new columns and the opt-out table are created at boot with `ADD COLUMN IF NOT EXISTS` / `CREATE TABLE IF NOT EXISTS` (MariaDB and PostgreSQL), so installs that do not run TypeORM migrations for plugins need no manual step. Installs that do can generate a migration as usual — the entity declares the same columns.
|
|
20
|
+
|
|
21
|
+
## [0.17.2] — 2026-09-10
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Per-channel recovery links.** `abandonment.storefrontBaseUrls` (channel code → storefront origin) makes a cart abandoned on a second storefront link back to that storefront; unlisted channels use `storefrontBaseUrl` as before.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- README: the storefront restore route must show the basket the way the storefront does (drawer or page) rather than assume a `/cart` route, and must treat an `ErrorResult` from `addItemToOrder` as a failed line. The reference storefront implementation now does both.
|
|
28
|
+
|
|
8
29
|
## [0.17.1] — 2026-09-02
|
|
9
30
|
|
|
10
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -64,6 +64,9 @@ export const config: VendureConfig = {
|
|
|
64
64
|
recoveryLinkSecret: process.env.HULO_ABANDONMENT_SECRET,
|
|
65
65
|
recoveryLinkTtlHours: 72,
|
|
66
66
|
storefrontBaseUrl: 'https://shop.example.com',
|
|
67
|
+
// Opt-out link signing (0.18.0). Falls back to
|
|
68
|
+
// recoveryLinkSecret, then signingSecret.
|
|
69
|
+
optOutSecret: process.env.HULO_ABANDONMENT_OPTOUT_SECRET,
|
|
67
70
|
},
|
|
68
71
|
}),
|
|
69
72
|
],
|
|
@@ -101,6 +104,8 @@ server-side scanners look those event types up by name.
|
|
|
101
104
|
| `hulo.checkoutCompleted(orderCode, totalMinor)` | on the thank-you page | closes any open `abandoned_cart` row for this session |
|
|
102
105
|
| `hulo.rageClick(selector)` / `hulo.deadClick(selector)` | fire yourself if you have a better signal than the auto-detector | rage-click / dead-click hot-spot lists |
|
|
103
106
|
| `hulo.restoreCart(token)` | on your `/cart/restore?t=...` route | rebuild a cart from a signed recovery link |
|
|
107
|
+
| `hulo.resumeCart(token)` | same route, when you can resume the visitor's open order | as above, plus `resumeOrderCode` while the bound order is still open (0.18.0) |
|
|
108
|
+
| `hulo.recoveryConverted(orderCode)` | on the thank-you page | attributes the order to the recovery link the visitor arrived through (0.18.0) |
|
|
104
109
|
|
|
105
110
|
Full payload shapes:
|
|
106
111
|
|
|
@@ -130,9 +135,26 @@ needs a route that:
|
|
|
130
135
|
|
|
131
136
|
1. Reads `?t=` from the URL
|
|
132
137
|
2. Calls `GET /ees/recover-cart?t=<token>` to fetch `{ items: [...] }`
|
|
138
|
+
(or `POST /ees/recover-cart/resume?t=<token>` — see below)
|
|
133
139
|
3. Re-adds each `{ variantId, qty }` via your Vendure order API (usually
|
|
134
|
-
`addItemToOrder(productVariantId, quantity)`)
|
|
135
|
-
|
|
140
|
+
`addItemToOrder(productVariantId, quantity)`) — check the result is an
|
|
141
|
+
`Order`, not an `ErrorResult` (out of stock, purchase limit…)
|
|
142
|
+
4. Shows the basket when done — open your cart drawer or navigate to your
|
|
143
|
+
cart page, whichever your storefront has (don't assume a `/cart` route)
|
|
144
|
+
5. On the thank-you page, calls `hulo.recoveryConverted(order.code)` (or
|
|
145
|
+
`POST /ees/recover-cart/converted { t, orderCode }`) so the cart is
|
|
146
|
+
attributed to the link — the helper remembers `t` in `sessionStorage`
|
|
147
|
+
from step 2, so this is a no-op for visitors who did not arrive through
|
|
148
|
+
a recovery link
|
|
149
|
+
|
|
150
|
+
**Resuming the visitor's order (0.18.0).** When the host mints the link
|
|
151
|
+
with `issueRecoveryLink(id, { resumeOrderCode })`, both recovery endpoints
|
|
152
|
+
return `orderCode`, `orderState` and `resumable`, and the `resume` endpoint
|
|
153
|
+
adds `resumeOrderCode` — non-null only while that order is still in
|
|
154
|
+
`AddingItems` / `ArrangingPayment`. The Shop API cannot adopt an order
|
|
155
|
+
anonymously, so treat it as a hint: a signed-in owner already has it as
|
|
156
|
+
their active order (skip the re-add), a guest gets the items re-added as
|
|
157
|
+
usual. Either way the `items` array is always present as the fallback.
|
|
136
158
|
|
|
137
159
|
Guard against silently overwriting a live cart — if the visitor
|
|
138
160
|
already has items, show a "you already have items in your cart"
|
|
@@ -232,9 +254,58 @@ The token is time-bounded (`recoveryLinkTtlHours`, default 72) and
|
|
|
232
254
|
non-reusable. The storefront exchanges it via
|
|
233
255
|
`GET /ees/recover-cart?t=<token>` to get back the persisted item list.
|
|
234
256
|
|
|
257
|
+
Multi-storefront installs: set `abandonment.storefrontBaseUrls` to a map of
|
|
258
|
+
channel code → storefront origin (for example
|
|
259
|
+
`{ licensedock: 'https://license-dock.com' }`) and each cart's link points
|
|
260
|
+
at the storefront it was abandoned on; channels not listed fall back to
|
|
261
|
+
`storefrontBaseUrl`.
|
|
262
|
+
|
|
235
263
|
Set `abandonment.recoveryLinkSecret` in plugin options to enable this —
|
|
236
264
|
without it, the endpoint returns `{ error: 'recovery-disabled-or-not-found' }`.
|
|
237
265
|
|
|
266
|
+
**Attribution (0.18.0).**
|
|
267
|
+
Every `abandoned_cart` row carries `recoveryStep` — `link_issued` →
|
|
268
|
+
`link_opened` → `resumed` → `converted`, never moving backwards — plus
|
|
269
|
+
`convertedAt`, `convertedOrderId`, `convertedOrderCode` and
|
|
270
|
+
`resumeOrderCode`. Steps advance as the link is minted, exchanged
|
|
271
|
+
(`recover-cart`), resumed (`recover-cart/resume`) and finally reported
|
|
272
|
+
converted by the storefront (`POST /ees/recover-cart/converted
|
|
273
|
+
{ t, orderCode }` — token-bound, the order must exist and be past
|
|
274
|
+
`AddingItems`). The scanner's own `checkout_completed` match still marks
|
|
275
|
+
rows `converted` and stamps `convertedAt`, but leaves `recoveryStep`
|
|
276
|
+
alone — so "converted via link" is exactly `recoveryStep = 'converted'`.
|
|
277
|
+
`GET /ees/abandoned-carts/summary` returns an `attribution` block:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{ "linkIssued": 120, "linkOpened": 41, "resumed": 9, "convertedViaLink": 14,
|
|
281
|
+
"convertedViaLinkValueMinor": 184950, "convertedTotal": 37, "optOuts": 3 }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Email opt-out (0.18.0).**
|
|
285
|
+
Recovery emails must carry an unsubscribe link. Build it with
|
|
286
|
+
`abandonedCartService.buildOptOutLink(email)` and add the headers from
|
|
287
|
+
`abandonedCartService.buildListUnsubscribeHeaders(email)`
|
|
288
|
+
(`List-Unsubscribe` + `List-Unsubscribe-Post: List-Unsubscribe=One-Click`)
|
|
289
|
+
so Gmail / Outlook / Yahoo show their native "Unsubscribe" button. Both
|
|
290
|
+
point at `GET|POST /ees/abandoned-carts/opt-out?e=<token>` on the Vendure
|
|
291
|
+
server: GET renders a small confirmation page, POST is the RFC 8058
|
|
292
|
+
one-click form. The token is `base64url(email).hmac(email)` signed with
|
|
293
|
+
`abandonment.optOutSecret` (default: `recoveryLinkSecret`, then
|
|
294
|
+
`signingSecret`) — nobody can unsubscribe someone else.
|
|
295
|
+
|
|
296
|
+
Before every send call `await abandonedCartService.isOptedOut(email)`;
|
|
297
|
+
it fails closed (a DB error counts as opted out). Opt-outs live in
|
|
298
|
+
`abandoned_cart_opt_out` (keyed by the SHA-256 of the lower-cased
|
|
299
|
+
address). Admin: `GET /ees/abandoned-carts/opt-outs`,
|
|
300
|
+
`POST /ees/abandoned-carts/opt-outs/remove { email }` after an explicit
|
|
301
|
+
customer request; `GET /ees/abandoned-carts/:id` reports `optedOut`.
|
|
302
|
+
|
|
303
|
+
**Schema.** The 0.18.0 columns and the opt-out table are added at boot
|
|
304
|
+
with `ADD COLUMN IF NOT EXISTS` / `CREATE TABLE IF NOT EXISTS` (MariaDB
|
|
305
|
+
and PostgreSQL). Installs that run TypeORM migrations for plugins can
|
|
306
|
+
generate one as usual — the `AbandonedCart` entity declares the same
|
|
307
|
+
columns, so the generator finds nothing to add once the plugin has booted.
|
|
308
|
+
|
|
238
309
|
**Slack notification.**
|
|
239
310
|
`abandonment.slackWebhookUrl` + `abandonment.slackMinValueMinor`
|
|
240
311
|
control an at-most-once Slack post per abandonment above the value
|
|
@@ -315,7 +386,10 @@ storefront origin):
|
|
|
315
386
|
| --- | --- | --- |
|
|
316
387
|
| `POST` | `/ees/track` | ingest a batch of visitor events |
|
|
317
388
|
| `GET` | `/ees/hulo.js` | typed storefront helper JS (since 0.8.1) |
|
|
318
|
-
| `GET` | `/ees/recover-cart?t=<token>` | resolve a recovery token → items |
|
|
389
|
+
| `GET` | `/ees/recover-cart?t=<token>` | resolve a recovery token → items (+ `orderCode`, `orderState`, `resumable` since 0.18.0); 30/min per IP |
|
|
390
|
+
| `POST` | `/ees/recover-cart/resume?t=<token>` | as above plus `resumeOrderCode` while the bound order is open (0.18.0); 30/min per IP |
|
|
391
|
+
| `POST` | `/ees/recover-cart/converted` | `{ t, orderCode }` — attribute a placed order to its recovery link (0.18.0); 10/min per IP |
|
|
392
|
+
| `GET`/`POST` | `/ees/abandoned-carts/opt-out?e=<token>` | email opt-out; POST is RFC 8058 one-click (0.18.0); 10/min per IP |
|
|
319
393
|
| `GET` | `/ees/recommendations/also-viewed?productId=…` | co-view recs |
|
|
320
394
|
| `GET` | `/ees/recommendations/personal?visitorId=…` | personalised recs |
|
|
321
395
|
| `GET` | `/ees/recommendations/trending?hours=…` | most-viewed products |
|
|
@@ -344,7 +418,9 @@ Vendure admin session cookie):
|
|
|
344
418
|
| `GET` | `/ees/abandoned-carts` | paginated list w/ filters (0.8.0) |
|
|
345
419
|
| `GET` | `/ees/abandoned-carts/summary` | totals + recovery rate (0.8.0) |
|
|
346
420
|
| `GET` | `/ees/abandoned-carts/:id` | detail incl. parsed items (0.8.0) |
|
|
347
|
-
| `POST` | `/ees/abandoned-carts/:id/recovery-link` | mint signed URL (0.8.0, `UpdateCustomer`) |
|
|
421
|
+
| `POST` | `/ees/abandoned-carts/:id/recovery-link` | mint signed URL (0.8.0, `UpdateCustomer`); body `{ resumeOrderCode? }` binds it to an order (0.18.0) |
|
|
422
|
+
| `GET` | `/ees/abandoned-carts/opt-outs` | opted-out addresses (0.18.0) |
|
|
423
|
+
| `POST` | `/ees/abandoned-carts/opt-outs/remove` | `{ email }` — re-enable after an explicit request (0.18.0, `UpdateCustomer`) |
|
|
348
424
|
| `POST` | `/ees/abandoned-carts/:id/status` | mark recovered/dismissed (0.8.0, `UpdateCustomer`) |
|
|
349
425
|
| `GET` | `/ees/abandoned-carts/export.csv` | CSV export (0.8.0) |
|
|
350
426
|
| `GET` | `/ees/recommendations/aggregate-now` | force co-view sweep (0.8.0, `SuperAdmin`) |
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OnApplicationBootstrap } from '@nestjs/common';
|
|
1
2
|
import { RequestContext } from '@vendure/core';
|
|
2
3
|
import { Request, Response } from 'express';
|
|
3
4
|
import { AbandonedCartService } from './abandoned-cart.service';
|
|
@@ -9,15 +10,26 @@ import { AbandonedCartService } from './abandoned-cart.service';
|
|
|
9
10
|
* GET /ees/abandoned-carts/summary — totals + top-value + recovery rate
|
|
10
11
|
* GET /ees/abandoned-carts/:id — detail incl. parsed items
|
|
11
12
|
* POST /ees/abandoned-carts/:id/recovery-link — mint/reissue signed URL
|
|
13
|
+
* (body `{ resumeOrderCode? }` binds it to an order, 0.18.0)
|
|
12
14
|
* POST /ees/abandoned-carts/:id/status — mark recovered/dismissed
|
|
13
15
|
* GET /ees/abandoned-carts/export.csv — CSV export
|
|
16
|
+
* GET /ees/abandoned-carts/opt-outs — opted-out addresses (0.18.0)
|
|
17
|
+
* POST /ees/abandoned-carts/opt-outs/remove — `{ email }` re-enable (0.18.0)
|
|
14
18
|
*
|
|
15
|
-
* Storefront-side (unauthenticated):
|
|
16
|
-
* GET /ees/recover-cart?t=... — resolve a token → items
|
|
19
|
+
* Storefront-side (unauthenticated, rate-limited per IP):
|
|
20
|
+
* GET /ees/recover-cart?t=... — resolve a token → items (+ orderCode / resumable, 0.18.0)
|
|
21
|
+
* POST /ees/recover-cart/resume?t=... — same, plus `resumeOrderCode` while the bound order is open (0.18.0)
|
|
22
|
+
* POST /ees/recover-cart/converted — `{ t, orderCode }` attribution when the restored cart checks out (0.18.0)
|
|
23
|
+
* GET|POST /ees/abandoned-carts/opt-out?e= — email opt-out (RFC 8058 one-click on POST) (0.18.0)
|
|
17
24
|
*/
|
|
18
|
-
export declare class AbandonedCartController {
|
|
25
|
+
export declare class AbandonedCartController implements OnApplicationBootstrap {
|
|
19
26
|
private readonly service;
|
|
27
|
+
private limiter;
|
|
20
28
|
constructor(service: AbandonedCartService);
|
|
29
|
+
onApplicationBootstrap(): void;
|
|
30
|
+
/** True (and 429 already written) when the caller is over budget.
|
|
31
|
+
* `cost` = 60 / per-minute allowance, so cost 2 → 30/min, cost 6 → 10/min. */
|
|
32
|
+
private rateLimited;
|
|
21
33
|
list(ctx: RequestContext, takeRaw?: string, skipRaw?: string, status?: string, minValueRaw?: string, email?: string): Promise<{
|
|
22
34
|
items: any;
|
|
23
35
|
total: number;
|
|
@@ -28,6 +40,15 @@ export declare class AbandonedCartController {
|
|
|
28
40
|
private buildItemsPreview;
|
|
29
41
|
summary(ctx: RequestContext, daysRaw?: string): Promise<{
|
|
30
42
|
windowDays: number;
|
|
43
|
+
attribution: {
|
|
44
|
+
linkIssued: number;
|
|
45
|
+
linkOpened: number;
|
|
46
|
+
resumed: number;
|
|
47
|
+
convertedViaLink: number;
|
|
48
|
+
convertedViaLinkValueMinor: number;
|
|
49
|
+
convertedTotal: number;
|
|
50
|
+
optOuts: number;
|
|
51
|
+
};
|
|
31
52
|
total: number;
|
|
32
53
|
openCount: number;
|
|
33
54
|
recoveredCount: number;
|
|
@@ -40,6 +61,32 @@ export declare class AbandonedCartController {
|
|
|
40
61
|
avgValueMinor: number;
|
|
41
62
|
}>;
|
|
42
63
|
exportCsv(ctx: RequestContext, res: Response, daysRaw?: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Email opt-out. Public, token-bound (`e=` is an HMAC of the address —
|
|
66
|
+
* see `buildOptOutToken`). GET renders a tiny confirmation page for
|
|
67
|
+
* humans clicking the footer link; POST is the RFC 8058 one-click form
|
|
68
|
+
* mail clients send when the user hits their native "Unsubscribe".
|
|
69
|
+
* Both are idempotent.
|
|
70
|
+
*/
|
|
71
|
+
optOutGet(req: Request, res: Response, tokenRaw?: string): Promise<void>;
|
|
72
|
+
optOutPost(req: Request, res: Response, tokenRaw?: string, body?: any): Promise<void>;
|
|
73
|
+
private applyOptOut;
|
|
74
|
+
private optOutPage;
|
|
75
|
+
listOptOuts(ctx: RequestContext, takeRaw?: string, skipRaw?: string): Promise<{
|
|
76
|
+
items: any[];
|
|
77
|
+
total: number;
|
|
78
|
+
take: number;
|
|
79
|
+
skip: number;
|
|
80
|
+
}>;
|
|
81
|
+
removeOptOut(ctx: RequestContext, body: {
|
|
82
|
+
email?: string;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
ok: boolean;
|
|
85
|
+
error?: undefined;
|
|
86
|
+
} | {
|
|
87
|
+
error: string;
|
|
88
|
+
ok?: undefined;
|
|
89
|
+
}>;
|
|
43
90
|
detail(ctx: RequestContext, idRaw: string): Promise<any>;
|
|
44
91
|
/**
|
|
45
92
|
* Fill in missing `name` (and, when possible, `productId`) on each
|
|
@@ -50,7 +97,9 @@ export declare class AbandonedCartController {
|
|
|
50
97
|
* cart was abandoned).
|
|
51
98
|
*/
|
|
52
99
|
private enrichItemsWithNames;
|
|
53
|
-
issueRecoveryLink(ctx: RequestContext, idRaw: string
|
|
100
|
+
issueRecoveryLink(ctx: RequestContext, idRaw: string, body?: {
|
|
101
|
+
resumeOrderCode?: string | null;
|
|
102
|
+
}): Promise<{
|
|
54
103
|
error: string;
|
|
55
104
|
hint: string;
|
|
56
105
|
ok?: undefined;
|
|
@@ -75,15 +124,22 @@ export declare class AbandonedCartController {
|
|
|
75
124
|
* of cart items the storefront can restore. Rate-limited by the
|
|
76
125
|
* plugin's usual ingest limiter (same origin as the tracker).
|
|
77
126
|
*/
|
|
78
|
-
recover(req: Request, token?: string): Promise<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
recover(req: Request, res: Response, token?: string): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Resume the exact order the link was bound to. Same payload as
|
|
130
|
+
* `recover-cart` plus `resumeOrderCode` — non-null only while that
|
|
131
|
+
* order is still `AddingItems` / `ArrangingPayment`. The storefront
|
|
132
|
+
* cannot adopt an order anonymously through the Shop API, so it
|
|
133
|
+
* should treat `resumeOrderCode` as a hint (e.g. sign-in prompt for
|
|
134
|
+
* the owner, or "your order S2BZ… is waiting") and fall back to
|
|
135
|
+
* re-adding `items` — which always works.
|
|
136
|
+
*/
|
|
137
|
+
resume(req: Request, res: Response, tokenQ?: string, body?: any): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Attribution — the restored cart checked out. Token-bound, so a
|
|
140
|
+
* stranger cannot mark carts converted; the order must exist and be
|
|
141
|
+
* past `AddingItems`. Idempotent.
|
|
142
|
+
*/
|
|
143
|
+
converted(req: Request, res: Response, tokenQ?: string, body?: any): Promise<void>;
|
|
88
144
|
}
|
|
89
145
|
//# sourceMappingURL=abandoned-cart.controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abandoned-cart.controller.d.ts","sourceRoot":"","sources":["../src/abandoned-cart.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"abandoned-cart.controller.d.ts","sourceRoot":"","sources":["../src/abandoned-cart.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuD,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7G,OAAO,EAAO,cAAc,EAAqB,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAIhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBACa,uBAAwB,YAAW,sBAAsB;IAGtD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,OAAO,CAA4B;gBAEd,OAAO,EAAE,oBAAoB;IAE1D,sBAAsB,IAAI,IAAI;IAO9B;mFAC+E;IAC/E,OAAO,CAAC,WAAW;IAab,IAAI,CACC,GAAG,EAAE,cAAc,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EACvB,KAAK,CAAC,EAAE,MAAM;;;;;;IA8DlC,uDAAuD;IACvD,OAAO,CAAC,iBAAiB;IAiBnB,OAAO,CAAQ,GAAG,EAAE,cAAc,EAAiB,OAAO,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;IAyCnE,SAAS,CAAQ,GAAG,EAAE,cAAc,EAAS,GAAG,EAAE,QAAQ,EAAiB,OAAO,CAAC,EAAE,MAAM;IA6BjG;;;;;;OAMG;IAEG,SAAS,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,EAAc,QAAQ,CAAC,EAAE,MAAM;IASlF,UAAU,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,EAAc,QAAQ,CAAC,EAAE,MAAM,EAAU,IAAI,CAAC,EAAE,GAAG;YAQ/F,WAAW;IASzB,OAAO,CAAC,UAAU;IAcZ,WAAW,CAAQ,GAAG,EAAE,cAAc,EAAiB,OAAO,CAAC,EAAE,MAAM,EAAiB,OAAO,CAAC,EAAE,MAAM;;;;;;IASxG,YAAY,CAAQ,GAAG,EAAE,cAAc,EAAU,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;IAOzE,MAAM,CAAQ,GAAG,EAAE,cAAc,EAAe,KAAK,EAAE,MAAM;IA0BnE;;;;;;;OAOG;YACW,oBAAoB;IAyE5B,iBAAiB,CACZ,GAAG,EAAE,cAAc,EACb,KAAK,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;;;;;;;;;IAahD,SAAS,CACJ,GAAG,EAAE,cAAc,EACb,KAAK,EAAE,MAAM,EAClB,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;IAWrC;;;;OAIG;IAEG,OAAO,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,EAAc,KAAK,CAAC,EAAE,MAAM;IAUnF;;;;;;;;OAQG;IAEG,MAAM,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,EAAc,MAAM,CAAC,EAAE,MAAM,EAAU,IAAI,CAAC,EAAE,GAAG;IAUvG;;;;OAIG;IAEG,SAAS,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ,EAAc,MAAM,CAAC,EAAE,MAAM,EAAU,IAAI,CAAC,EAAE,GAAG;CAS7G"}
|
|
@@ -15,7 +15,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.AbandonedCartController = void 0;
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const core_1 = require("@vendure/core");
|
|
18
|
+
const vendure_licence_sdk_1 = require("@huloglobal/vendure-licence-sdk");
|
|
18
19
|
const abandoned_cart_service_1 = require("./abandoned-cart.service");
|
|
20
|
+
const proxy_headers_1 = require("./proxy-headers");
|
|
21
|
+
const recovery_tokens_1 = require("./recovery-tokens");
|
|
19
22
|
/**
|
|
20
23
|
* Admin API for the Abandoned Cart feature.
|
|
21
24
|
*
|
|
@@ -24,15 +27,41 @@ const abandoned_cart_service_1 = require("./abandoned-cart.service");
|
|
|
24
27
|
* GET /ees/abandoned-carts/summary — totals + top-value + recovery rate
|
|
25
28
|
* GET /ees/abandoned-carts/:id — detail incl. parsed items
|
|
26
29
|
* POST /ees/abandoned-carts/:id/recovery-link — mint/reissue signed URL
|
|
30
|
+
* (body `{ resumeOrderCode? }` binds it to an order, 0.18.0)
|
|
27
31
|
* POST /ees/abandoned-carts/:id/status — mark recovered/dismissed
|
|
28
32
|
* GET /ees/abandoned-carts/export.csv — CSV export
|
|
33
|
+
* GET /ees/abandoned-carts/opt-outs — opted-out addresses (0.18.0)
|
|
34
|
+
* POST /ees/abandoned-carts/opt-outs/remove — `{ email }` re-enable (0.18.0)
|
|
29
35
|
*
|
|
30
|
-
* Storefront-side (unauthenticated):
|
|
31
|
-
* GET /ees/recover-cart?t=... — resolve a token → items
|
|
36
|
+
* Storefront-side (unauthenticated, rate-limited per IP):
|
|
37
|
+
* GET /ees/recover-cart?t=... — resolve a token → items (+ orderCode / resumable, 0.18.0)
|
|
38
|
+
* POST /ees/recover-cart/resume?t=... — same, plus `resumeOrderCode` while the bound order is open (0.18.0)
|
|
39
|
+
* POST /ees/recover-cart/converted — `{ t, orderCode }` attribution when the restored cart checks out (0.18.0)
|
|
40
|
+
* GET|POST /ees/abandoned-carts/opt-out?e= — email opt-out (RFC 8058 one-click on POST) (0.18.0)
|
|
32
41
|
*/
|
|
33
42
|
let AbandonedCartController = class AbandonedCartController {
|
|
34
43
|
constructor(service) {
|
|
35
44
|
this.service = service;
|
|
45
|
+
this.limiter = null;
|
|
46
|
+
}
|
|
47
|
+
onApplicationBootstrap() {
|
|
48
|
+
// One shared bucket map; the bucket name is part of the key so each
|
|
49
|
+
// endpoint gets its own allowance. Capacity is the most generous of
|
|
50
|
+
// the per-endpoint limits — `cost` scales the others down.
|
|
51
|
+
this.limiter = new vendure_licence_sdk_1.RateLimiter({ capacity: 60, windowMs: 60000 });
|
|
52
|
+
}
|
|
53
|
+
/** True (and 429 already written) when the caller is over budget.
|
|
54
|
+
* `cost` = 60 / per-minute allowance, so cost 2 → 30/min, cost 6 → 10/min. */
|
|
55
|
+
rateLimited(req, res, bucket, cost) {
|
|
56
|
+
const ip = (0, proxy_headers_1.getRealIp)(req) || '';
|
|
57
|
+
if (!ip || !this.limiter)
|
|
58
|
+
return false;
|
|
59
|
+
if (!this.limiter.allow(`${bucket}|${ip}`, cost)) {
|
|
60
|
+
res.setHeader('Retry-After', '60');
|
|
61
|
+
res.status(429).json({ error: 'rate-limited' });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
36
65
|
}
|
|
37
66
|
async list(ctx, takeRaw, skipRaw, status, minValueRaw, email) {
|
|
38
67
|
var _a;
|
|
@@ -65,6 +94,7 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
65
94
|
// so the payload growth stays proportional to the page size.
|
|
66
95
|
`SELECT id, sessionId, visitorId, customerId, currency, totalMinor, itemCount,
|
|
67
96
|
itemsJson, email, status, abandonedAt, recoveredAt, notificationSent,
|
|
97
|
+
resumeOrderCode, recoveryStep, convertedAt, convertedOrderId, convertedOrderCode,
|
|
68
98
|
utmSource, utmMedium, utmCampaign, countryCode, regionCode, city,
|
|
69
99
|
ip, ipHash, userAgent, browser, deviceType,
|
|
70
100
|
landingUrl, lastKnownUrl, lastKnownReferrer,
|
|
@@ -135,8 +165,10 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
135
165
|
const s = (rows === null || rows === void 0 ? void 0 : rows[0]) || {};
|
|
136
166
|
const total = Number(s.total || 0);
|
|
137
167
|
const rec = Number(s.recoveredCount || 0) + Number(s.convertedCount || 0);
|
|
168
|
+
const attribution = await this.service.attributionSummary(since);
|
|
138
169
|
return {
|
|
139
170
|
windowDays: days,
|
|
171
|
+
attribution,
|
|
140
172
|
total,
|
|
141
173
|
openCount: Number(s.openCount || 0),
|
|
142
174
|
recoveredCount: Number(s.recoveredCount || 0),
|
|
@@ -173,6 +205,60 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
173
205
|
}
|
|
174
206
|
res.end();
|
|
175
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Email opt-out. Public, token-bound (`e=` is an HMAC of the address —
|
|
210
|
+
* see `buildOptOutToken`). GET renders a tiny confirmation page for
|
|
211
|
+
* humans clicking the footer link; POST is the RFC 8058 one-click form
|
|
212
|
+
* mail clients send when the user hits their native "Unsubscribe".
|
|
213
|
+
* Both are idempotent.
|
|
214
|
+
*/
|
|
215
|
+
async optOutGet(req, res, tokenRaw) {
|
|
216
|
+
if (this.rateLimited(req, res, 'opt-out', 6))
|
|
217
|
+
return;
|
|
218
|
+
const result = await this.applyOptOut(req, tokenRaw);
|
|
219
|
+
res.setHeader('cache-control', 'no-store');
|
|
220
|
+
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
221
|
+
res.status(result.ok ? 200 : 400).send(this.optOutPage(result.ok));
|
|
222
|
+
}
|
|
223
|
+
async optOutPost(req, res, tokenRaw, body) {
|
|
224
|
+
if (this.rateLimited(req, res, 'opt-out', 6))
|
|
225
|
+
return;
|
|
226
|
+
const token = tokenRaw || (body === null || body === void 0 ? void 0 : body.e) || (typeof body === 'string' ? '' : undefined);
|
|
227
|
+
const result = await this.applyOptOut(req, token);
|
|
228
|
+
res.setHeader('cache-control', 'no-store');
|
|
229
|
+
res.status(result.ok ? 200 : 400).json(result);
|
|
230
|
+
}
|
|
231
|
+
async applyOptOut(req, tokenRaw) {
|
|
232
|
+
const secret = this.service.getOptOutSecret();
|
|
233
|
+
if (!secret)
|
|
234
|
+
return { ok: false, error: 'opt-out-disabled' };
|
|
235
|
+
const email = (0, recovery_tokens_1.verifyOptOutToken)(tokenRaw, secret);
|
|
236
|
+
if (!email)
|
|
237
|
+
return { ok: false, error: 'invalid-token' };
|
|
238
|
+
await this.service.optOut(email, { source: 'link', ip: (0, proxy_headers_1.getRealIp)(req) });
|
|
239
|
+
return { ok: true };
|
|
240
|
+
}
|
|
241
|
+
optOutPage(ok) {
|
|
242
|
+
const title = ok ? 'You have been unsubscribed' : 'This link is not valid';
|
|
243
|
+
const body = ok
|
|
244
|
+
? 'We will not send you any more reminders about items left in your basket. You can close this page.'
|
|
245
|
+
: 'The unsubscribe link is incomplete or has been altered. Please use the link exactly as it appears in the email.';
|
|
246
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">`
|
|
247
|
+
+ `<meta name="robots" content="noindex"><title>${title}</title>`
|
|
248
|
+
+ `<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#f6f7f9;color:#1b1f24;margin:0;display:flex;min-height:100vh;align-items:center;justify-content:center}`
|
|
249
|
+
+ `main{background:#fff;border:1px solid #e3e6ea;border-radius:12px;padding:32px 36px;max-width:440px;box-shadow:0 2px 12px rgba(0,0,0,.05)}h1{font-size:20px;margin:0 0 12px}p{margin:0;line-height:1.5;color:#4b5563}</style>`
|
|
250
|
+
+ `</head><body><main><h1>${title}</h1><p>${body}</p></main></body></html>`;
|
|
251
|
+
}
|
|
252
|
+
async listOptOuts(ctx, takeRaw, skipRaw) {
|
|
253
|
+
const take = Math.min(Math.max(1, parseInt(takeRaw || '50', 10) || 50), 500);
|
|
254
|
+
const skip = Math.max(0, parseInt(skipRaw || '0', 10) || 0);
|
|
255
|
+
const { items, total } = await this.service.listOptOuts(take, skip);
|
|
256
|
+
return { items, total, take, skip };
|
|
257
|
+
}
|
|
258
|
+
async removeOptOut(ctx, body) {
|
|
259
|
+
const ok = await this.service.optIn(String((body === null || body === void 0 ? void 0 : body.email) || ''));
|
|
260
|
+
return ok ? { ok: true } : { error: 'not-found' };
|
|
261
|
+
}
|
|
176
262
|
async detail(ctx, idRaw) {
|
|
177
263
|
const id = parseInt(idRaw, 10);
|
|
178
264
|
const conn = this.service.connection.rawConnection;
|
|
@@ -191,10 +277,12 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
191
277
|
// before the product had a translation, or third-party
|
|
192
278
|
// integrations that fire cart_snapshot without a name field.
|
|
193
279
|
const enrichedItems = await this.enrichItemsWithNames(items);
|
|
280
|
+
const optedOut = r.email ? await this.service.isOptedOut(r.email) : false;
|
|
194
281
|
return {
|
|
195
282
|
...r,
|
|
196
283
|
items: enrichedItems,
|
|
197
284
|
itemsPreview: this.buildItemsPreview(enrichedItems),
|
|
285
|
+
optedOut,
|
|
198
286
|
};
|
|
199
287
|
}
|
|
200
288
|
/**
|
|
@@ -279,9 +367,12 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
279
367
|
return it;
|
|
280
368
|
});
|
|
281
369
|
}
|
|
282
|
-
async issueRecoveryLink(ctx, idRaw) {
|
|
370
|
+
async issueRecoveryLink(ctx, idRaw, body) {
|
|
283
371
|
const id = parseInt(idRaw, 10);
|
|
284
|
-
const
|
|
372
|
+
const options = body && body.resumeOrderCode !== undefined
|
|
373
|
+
? { resumeOrderCode: body.resumeOrderCode === null ? null : (0, recovery_tokens_1.sanitiseOrderCode)(body.resumeOrderCode) || null }
|
|
374
|
+
: {};
|
|
375
|
+
const url = await this.service.issueRecoveryLink(id, options);
|
|
285
376
|
if (!url)
|
|
286
377
|
return { error: 'recovery-disabled-or-not-found', hint: 'Set abandonment.recoveryLinkSecret in plugin options' };
|
|
287
378
|
return { ok: true, url };
|
|
@@ -300,14 +391,64 @@ let AbandonedCartController = class AbandonedCartController {
|
|
|
300
391
|
* of cart items the storefront can restore. Rate-limited by the
|
|
301
392
|
* plugin's usual ingest limiter (same origin as the tracker).
|
|
302
393
|
*/
|
|
303
|
-
async recover(req, token) {
|
|
394
|
+
async recover(req, res, token) {
|
|
395
|
+
if (this.rateLimited(req, res, 'recover', 2))
|
|
396
|
+
return;
|
|
397
|
+
res.setHeader('cache-control', 'no-store');
|
|
304
398
|
const t = String(token || '').trim();
|
|
305
|
-
if (!t)
|
|
306
|
-
|
|
399
|
+
if (!t) {
|
|
400
|
+
res.status(400).json({ error: 'missing-token' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
307
403
|
const result = await this.service.findByRecoveryToken(t);
|
|
308
|
-
if (!result)
|
|
309
|
-
|
|
310
|
-
|
|
404
|
+
if (!result) {
|
|
405
|
+
res.json({ error: 'expired-or-invalid' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
res.json({ ok: true, ...result });
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resume the exact order the link was bound to. Same payload as
|
|
412
|
+
* `recover-cart` plus `resumeOrderCode` — non-null only while that
|
|
413
|
+
* order is still `AddingItems` / `ArrangingPayment`. The storefront
|
|
414
|
+
* cannot adopt an order anonymously through the Shop API, so it
|
|
415
|
+
* should treat `resumeOrderCode` as a hint (e.g. sign-in prompt for
|
|
416
|
+
* the owner, or "your order S2BZ… is waiting") and fall back to
|
|
417
|
+
* re-adding `items` — which always works.
|
|
418
|
+
*/
|
|
419
|
+
async resume(req, res, tokenQ, body) {
|
|
420
|
+
if (this.rateLimited(req, res, 'recover', 2))
|
|
421
|
+
return;
|
|
422
|
+
res.setHeader('cache-control', 'no-store');
|
|
423
|
+
const t = String(tokenQ || (body === null || body === void 0 ? void 0 : body.t) || '').trim();
|
|
424
|
+
if (!t) {
|
|
425
|
+
res.status(400).json({ error: 'missing-token' });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const result = await this.service.resumeByRecoveryToken(t);
|
|
429
|
+
if (!result) {
|
|
430
|
+
res.json({ error: 'expired-or-invalid' });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
res.json({ ok: true, ...result });
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Attribution — the restored cart checked out. Token-bound, so a
|
|
437
|
+
* stranger cannot mark carts converted; the order must exist and be
|
|
438
|
+
* past `AddingItems`. Idempotent.
|
|
439
|
+
*/
|
|
440
|
+
async converted(req, res, tokenQ, body) {
|
|
441
|
+
if (this.rateLimited(req, res, 'converted', 6))
|
|
442
|
+
return;
|
|
443
|
+
res.setHeader('cache-control', 'no-store');
|
|
444
|
+
const t = String(tokenQ || (body === null || body === void 0 ? void 0 : body.t) || '').trim();
|
|
445
|
+
const orderCode = (0, recovery_tokens_1.sanitiseOrderCode)(body === null || body === void 0 ? void 0 : body.orderCode);
|
|
446
|
+
if (!t || !orderCode) {
|
|
447
|
+
res.status(400).json({ error: 'missing-token-or-order-code' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const result = await this.service.markConvertedByToken(t, orderCode);
|
|
451
|
+
res.status(result.ok ? 200 : 400).json(result);
|
|
311
452
|
}
|
|
312
453
|
};
|
|
313
454
|
exports.AbandonedCartController = AbandonedCartController;
|
|
@@ -343,6 +484,44 @@ __decorate([
|
|
|
343
484
|
__metadata("design:paramtypes", [core_1.RequestContext, Object, String]),
|
|
344
485
|
__metadata("design:returntype", Promise)
|
|
345
486
|
], AbandonedCartController.prototype, "exportCsv", null);
|
|
487
|
+
__decorate([
|
|
488
|
+
(0, common_1.Get)('abandoned-carts/opt-out'),
|
|
489
|
+
__param(0, (0, common_1.Req)()),
|
|
490
|
+
__param(1, (0, common_1.Res)()),
|
|
491
|
+
__param(2, (0, common_1.Query)('e')),
|
|
492
|
+
__metadata("design:type", Function),
|
|
493
|
+
__metadata("design:paramtypes", [Object, Object, String]),
|
|
494
|
+
__metadata("design:returntype", Promise)
|
|
495
|
+
], AbandonedCartController.prototype, "optOutGet", null);
|
|
496
|
+
__decorate([
|
|
497
|
+
(0, common_1.Post)('abandoned-carts/opt-out'),
|
|
498
|
+
__param(0, (0, common_1.Req)()),
|
|
499
|
+
__param(1, (0, common_1.Res)()),
|
|
500
|
+
__param(2, (0, common_1.Query)('e')),
|
|
501
|
+
__param(3, (0, common_1.Body)()),
|
|
502
|
+
__metadata("design:type", Function),
|
|
503
|
+
__metadata("design:paramtypes", [Object, Object, String, Object]),
|
|
504
|
+
__metadata("design:returntype", Promise)
|
|
505
|
+
], AbandonedCartController.prototype, "optOutPost", null);
|
|
506
|
+
__decorate([
|
|
507
|
+
(0, common_1.Get)('abandoned-carts/opt-outs'),
|
|
508
|
+
(0, core_1.Allow)(core_1.Permission.ReadCustomer),
|
|
509
|
+
__param(0, (0, core_1.Ctx)()),
|
|
510
|
+
__param(1, (0, common_1.Query)('take')),
|
|
511
|
+
__param(2, (0, common_1.Query)('skip')),
|
|
512
|
+
__metadata("design:type", Function),
|
|
513
|
+
__metadata("design:paramtypes", [core_1.RequestContext, String, String]),
|
|
514
|
+
__metadata("design:returntype", Promise)
|
|
515
|
+
], AbandonedCartController.prototype, "listOptOuts", null);
|
|
516
|
+
__decorate([
|
|
517
|
+
(0, common_1.Post)('abandoned-carts/opt-outs/remove'),
|
|
518
|
+
(0, core_1.Allow)(core_1.Permission.UpdateCustomer),
|
|
519
|
+
__param(0, (0, core_1.Ctx)()),
|
|
520
|
+
__param(1, (0, common_1.Body)()),
|
|
521
|
+
__metadata("design:type", Function),
|
|
522
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object]),
|
|
523
|
+
__metadata("design:returntype", Promise)
|
|
524
|
+
], AbandonedCartController.prototype, "removeOptOut", null);
|
|
346
525
|
__decorate([
|
|
347
526
|
(0, common_1.Get)('abandoned-carts/:id'),
|
|
348
527
|
(0, core_1.Allow)(core_1.Permission.ReadCustomer),
|
|
@@ -357,8 +536,9 @@ __decorate([
|
|
|
357
536
|
(0, core_1.Allow)(core_1.Permission.UpdateCustomer),
|
|
358
537
|
__param(0, (0, core_1.Ctx)()),
|
|
359
538
|
__param(1, (0, common_1.Param)('id')),
|
|
539
|
+
__param(2, (0, common_1.Body)()),
|
|
360
540
|
__metadata("design:type", Function),
|
|
361
|
-
__metadata("design:paramtypes", [core_1.RequestContext, String]),
|
|
541
|
+
__metadata("design:paramtypes", [core_1.RequestContext, String, Object]),
|
|
362
542
|
__metadata("design:returntype", Promise)
|
|
363
543
|
], AbandonedCartController.prototype, "issueRecoveryLink", null);
|
|
364
544
|
__decorate([
|
|
@@ -374,11 +554,32 @@ __decorate([
|
|
|
374
554
|
__decorate([
|
|
375
555
|
(0, common_1.Get)('recover-cart'),
|
|
376
556
|
__param(0, (0, common_1.Req)()),
|
|
377
|
-
__param(1, (0, common_1.
|
|
557
|
+
__param(1, (0, common_1.Res)()),
|
|
558
|
+
__param(2, (0, common_1.Query)('t')),
|
|
378
559
|
__metadata("design:type", Function),
|
|
379
|
-
__metadata("design:paramtypes", [Object, String]),
|
|
560
|
+
__metadata("design:paramtypes", [Object, Object, String]),
|
|
380
561
|
__metadata("design:returntype", Promise)
|
|
381
562
|
], AbandonedCartController.prototype, "recover", null);
|
|
563
|
+
__decorate([
|
|
564
|
+
(0, common_1.Post)('recover-cart/resume'),
|
|
565
|
+
__param(0, (0, common_1.Req)()),
|
|
566
|
+
__param(1, (0, common_1.Res)()),
|
|
567
|
+
__param(2, (0, common_1.Query)('t')),
|
|
568
|
+
__param(3, (0, common_1.Body)()),
|
|
569
|
+
__metadata("design:type", Function),
|
|
570
|
+
__metadata("design:paramtypes", [Object, Object, String, Object]),
|
|
571
|
+
__metadata("design:returntype", Promise)
|
|
572
|
+
], AbandonedCartController.prototype, "resume", null);
|
|
573
|
+
__decorate([
|
|
574
|
+
(0, common_1.Post)('recover-cart/converted'),
|
|
575
|
+
__param(0, (0, common_1.Req)()),
|
|
576
|
+
__param(1, (0, common_1.Res)()),
|
|
577
|
+
__param(2, (0, common_1.Query)('t')),
|
|
578
|
+
__param(3, (0, common_1.Body)()),
|
|
579
|
+
__metadata("design:type", Function),
|
|
580
|
+
__metadata("design:paramtypes", [Object, Object, String, Object]),
|
|
581
|
+
__metadata("design:returntype", Promise)
|
|
582
|
+
], AbandonedCartController.prototype, "converted", null);
|
|
382
583
|
exports.AbandonedCartController = AbandonedCartController = __decorate([
|
|
383
584
|
(0, common_1.Controller)('ees'),
|
|
384
585
|
__metadata("design:paramtypes", [abandoned_cart_service_1.AbandonedCartService])
|