fresh-squeezy 0.1.11 → 0.1.13
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.es.md +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +677 -622
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +605 -578
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -185
- package/dist/index.d.ts +264 -185
- package/dist/index.js +597 -578
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/assets/fresh-squeezy-hero.png +0 -0
- package/bin/fresh-squeezy.js +0 -2
package/dist/index.js
CHANGED
|
@@ -160,24 +160,12 @@ function extractJsonApiErrors(body) {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// src/resources/users.ts
|
|
164
|
-
async function getAuthenticatedUser(http) {
|
|
165
|
-
return http.request({ path: "/v1/users/me" });
|
|
166
|
-
}
|
|
167
|
-
function userResource(doc) {
|
|
168
|
-
return doc.data;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
163
|
// src/core/mode.ts
|
|
172
164
|
function resolveActualMode(testMode) {
|
|
173
165
|
if (testMode === true) return "test";
|
|
174
166
|
if (testMode === false) return "live";
|
|
175
167
|
return void 0;
|
|
176
168
|
}
|
|
177
|
-
async function fetchActualMode(http) {
|
|
178
|
-
const doc = await getAuthenticatedUser(http);
|
|
179
|
-
return resolveActualMode(doc.meta?.test_mode);
|
|
180
|
-
}
|
|
181
169
|
|
|
182
170
|
// src/resources/stores.ts
|
|
183
171
|
async function getStore(http, storeId) {
|
|
@@ -187,6 +175,14 @@ async function listStores(http) {
|
|
|
187
175
|
return http.paginate("/v1/stores");
|
|
188
176
|
}
|
|
189
177
|
|
|
178
|
+
// src/resources/users.ts
|
|
179
|
+
async function getAuthenticatedUser(http) {
|
|
180
|
+
return http.request({ path: "/v1/users/me" });
|
|
181
|
+
}
|
|
182
|
+
function userResource(doc) {
|
|
183
|
+
return doc.data;
|
|
184
|
+
}
|
|
185
|
+
|
|
190
186
|
// src/validate/rules.ts
|
|
191
187
|
var ISSUE_CODES = {
|
|
192
188
|
AUTH_FAILED: "AUTH_FAILED",
|
|
@@ -275,20 +271,20 @@ function mapErrorToIssue(err, mapping = {}) {
|
|
|
275
271
|
return issue(ISSUE_CODES.UNKNOWN, "error", message);
|
|
276
272
|
}
|
|
277
273
|
async function probeFetch(fetcher, options) {
|
|
274
|
+
return probeCollection(fetcher, {
|
|
275
|
+
notFound: {
|
|
276
|
+
code: options.notFoundCode,
|
|
277
|
+
message: options.notFoundMessage,
|
|
278
|
+
...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
|
|
279
|
+
...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function probeCollection(fetcher, mapping = {}) {
|
|
278
284
|
try {
|
|
279
285
|
return { ok: true, resource: await fetcher() };
|
|
280
286
|
} catch (err) {
|
|
281
|
-
return {
|
|
282
|
-
ok: false,
|
|
283
|
-
issue: mapErrorToIssue(err, {
|
|
284
|
-
notFound: {
|
|
285
|
-
code: options.notFoundCode,
|
|
286
|
-
message: options.notFoundMessage,
|
|
287
|
-
...options.notFoundFix !== void 0 ? { suggestedFix: options.notFoundFix } : {},
|
|
288
|
-
...options.notFoundContext !== void 0 ? { context: options.notFoundContext } : {}
|
|
289
|
-
}
|
|
290
|
-
})
|
|
291
|
-
};
|
|
287
|
+
return { ok: false, issue: mapErrorToIssue(err, mapping) };
|
|
292
288
|
}
|
|
293
289
|
}
|
|
294
290
|
function checkStoreOwnership(check) {
|
|
@@ -314,187 +310,538 @@ function checkStoreOwnership(check) {
|
|
|
314
310
|
// src/validate/connection.ts
|
|
315
311
|
async function validateConnection(http, mode) {
|
|
316
312
|
const issues = [];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
issues.push(
|
|
330
|
-
issue(
|
|
331
|
-
ISSUE_CODES.MODE_MISMATCH,
|
|
332
|
-
"error",
|
|
333
|
-
`API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
|
|
334
|
-
{
|
|
335
|
-
suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
|
|
336
|
-
context: { declared: mode, actual: actualMode }
|
|
337
|
-
}
|
|
338
|
-
)
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
if (stores.length === 0) {
|
|
342
|
-
issues.push(
|
|
343
|
-
issue(
|
|
344
|
-
ISSUE_CODES.STORE_NOT_FOUND,
|
|
345
|
-
"warning",
|
|
346
|
-
"API key authenticated but no stores are reachable.",
|
|
347
|
-
{ suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
|
|
348
|
-
)
|
|
349
|
-
);
|
|
313
|
+
const fetched = await probeCollection(
|
|
314
|
+
async () => {
|
|
315
|
+
const userDoc2 = await getAuthenticatedUser(http);
|
|
316
|
+
const stores2 = await listStores(http);
|
|
317
|
+
return { userDoc: userDoc2, stores: stores2 };
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
unauthorized: {
|
|
321
|
+
code: ISSUE_CODES.AUTH_FAILED,
|
|
322
|
+
message: "API key rejected by Lemon Squeezy.",
|
|
323
|
+
suggestedFix: "Regenerate the key at https://app.lemonsqueezy.com/settings/api."
|
|
324
|
+
}
|
|
350
325
|
}
|
|
351
|
-
|
|
352
|
-
|
|
326
|
+
);
|
|
327
|
+
if (!fetched.ok) {
|
|
328
|
+
issues.push(fetched.issue);
|
|
329
|
+
return buildResult("connection", mode, issues);
|
|
330
|
+
}
|
|
331
|
+
const { userDoc, stores } = fetched.resource;
|
|
332
|
+
const actualMode = resolveActualMode(userDoc.meta?.test_mode);
|
|
333
|
+
const summary = {
|
|
334
|
+
user: userDoc.data.attributes,
|
|
335
|
+
storeCount: stores.length,
|
|
336
|
+
storeIds: stores.map((store) => store.id),
|
|
337
|
+
declaredMode: mode,
|
|
338
|
+
...actualMode ? { actualMode } : {}
|
|
339
|
+
};
|
|
340
|
+
if (actualMode && actualMode !== mode) {
|
|
353
341
|
issues.push(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
342
|
+
issue(
|
|
343
|
+
ISSUE_CODES.MODE_MISMATCH,
|
|
344
|
+
"error",
|
|
345
|
+
`API key is a ${actualMode}-mode key but was run with --mode ${mode}.`,
|
|
346
|
+
{
|
|
347
|
+
suggestedFix: `Either pass --mode ${actualMode} or use a ${mode}-mode key from https://app.lemonsqueezy.com/settings/api.`,
|
|
348
|
+
context: { declared: mode, actual: actualMode }
|
|
359
349
|
}
|
|
360
|
-
|
|
350
|
+
)
|
|
361
351
|
);
|
|
362
|
-
return buildResult("connection", mode, issues);
|
|
363
352
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
notFoundContext: { storeId: String(storeId) }
|
|
374
|
-
});
|
|
375
|
-
if (!fetched.ok) {
|
|
376
|
-
issues.push(fetched.issue);
|
|
377
|
-
return buildResult("store", mode, issues, void 0, {
|
|
378
|
-
label: `store ${storeId}`,
|
|
379
|
-
id: String(storeId)
|
|
380
|
-
});
|
|
353
|
+
if (stores.length === 0) {
|
|
354
|
+
issues.push(
|
|
355
|
+
issue(
|
|
356
|
+
ISSUE_CODES.STORE_NOT_FOUND,
|
|
357
|
+
"warning",
|
|
358
|
+
"API key authenticated but no stores are reachable.",
|
|
359
|
+
{ suggestedFix: "Confirm the API key belongs to an account that owns at least one store." }
|
|
360
|
+
)
|
|
361
|
+
);
|
|
381
362
|
}
|
|
382
|
-
return buildResult("
|
|
383
|
-
label: fetched.resource.attributes.name,
|
|
384
|
-
id: String(storeId)
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// src/resources/products.ts
|
|
389
|
-
async function getProduct(http, productId) {
|
|
390
|
-
return http.getResource(`/v1/products/${productId}`);
|
|
363
|
+
return buildResult("connection", mode, issues, summary);
|
|
391
364
|
}
|
|
392
|
-
async function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
});
|
|
365
|
+
async function fetchActualMode(http) {
|
|
366
|
+
const doc = await getAuthenticatedUser(http);
|
|
367
|
+
return resolveActualMode(doc.meta?.test_mode);
|
|
396
368
|
}
|
|
397
369
|
|
|
398
|
-
// src/resources/
|
|
399
|
-
async function
|
|
400
|
-
return http.getResource(`/v1/
|
|
401
|
-
}
|
|
402
|
-
async function listVariantsForProduct(http, productId) {
|
|
403
|
-
return http.paginate("/v1/variants", {
|
|
404
|
-
"filter[product_id]": String(productId)
|
|
405
|
-
});
|
|
370
|
+
// src/resources/discounts.ts
|
|
371
|
+
async function getDiscount(http, discountId) {
|
|
372
|
+
return http.getResource(`/v1/discounts/${discountId}`);
|
|
406
373
|
}
|
|
407
374
|
|
|
408
|
-
// src/validate/
|
|
409
|
-
async function
|
|
375
|
+
// src/validate/discount.ts
|
|
376
|
+
async function validateDiscount(http, mode, options) {
|
|
410
377
|
const issues = [];
|
|
411
|
-
const fetched = await probeFetch(() =>
|
|
412
|
-
notFoundCode: ISSUE_CODES.
|
|
413
|
-
notFoundMessage: `
|
|
414
|
-
notFoundFix: "Verify the
|
|
415
|
-
notFoundContext: {
|
|
378
|
+
const fetched = await probeFetch(() => getDiscount(http, options.discountId), {
|
|
379
|
+
notFoundCode: ISSUE_CODES.DISCOUNT_NOT_FOUND,
|
|
380
|
+
notFoundMessage: `Discount ${options.discountId} not found.`,
|
|
381
|
+
notFoundFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
|
|
382
|
+
notFoundContext: { discountId: String(options.discountId) }
|
|
416
383
|
});
|
|
417
384
|
if (!fetched.ok) {
|
|
418
385
|
issues.push(fetched.issue);
|
|
419
|
-
return buildResult("
|
|
420
|
-
label: `
|
|
421
|
-
id: String(options.
|
|
386
|
+
return buildResult("discount", mode, issues, void 0, {
|
|
387
|
+
label: `discount ${options.discountId}`,
|
|
388
|
+
id: String(options.discountId)
|
|
422
389
|
});
|
|
423
390
|
}
|
|
424
391
|
const attrs = fetched.resource.attributes;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
392
|
+
issues.push(...checkDiscount(attrs, { storeId: options.storeId }));
|
|
393
|
+
return buildResult("discount", mode, issues, attrs, {
|
|
394
|
+
label: `${attrs.name} (${attrs.code})`,
|
|
395
|
+
id: String(options.discountId)
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function checkDiscount(attrs, options) {
|
|
399
|
+
const issues = [];
|
|
400
|
+
const mismatch = checkStoreOwnership({
|
|
401
|
+
expectedStoreId: options.storeId,
|
|
402
|
+
actualStoreId: attrs.store_id,
|
|
403
|
+
code: ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
|
|
404
|
+
label: "Discount",
|
|
405
|
+
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores."
|
|
406
|
+
});
|
|
407
|
+
if (mismatch) issues.push(mismatch);
|
|
408
|
+
if (attrs.status === "draft") {
|
|
409
|
+
issues.push(
|
|
410
|
+
issue(
|
|
411
|
+
ISSUE_CODES.DISCOUNT_DRAFT,
|
|
412
|
+
"warning",
|
|
413
|
+
`Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
|
|
414
|
+
{
|
|
415
|
+
suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
|
|
416
|
+
context: { name: attrs.name, code: attrs.code }
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
);
|
|
434
420
|
}
|
|
435
|
-
|
|
421
|
+
const now = /* @__PURE__ */ new Date();
|
|
422
|
+
if (attrs.expires_at && new Date(attrs.expires_at) < now) {
|
|
436
423
|
issues.push(
|
|
437
424
|
issue(
|
|
438
|
-
ISSUE_CODES.
|
|
425
|
+
ISSUE_CODES.DISCOUNT_EXPIRED,
|
|
439
426
|
"error",
|
|
440
|
-
`
|
|
427
|
+
`Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
|
|
441
428
|
{
|
|
442
|
-
suggestedFix: "
|
|
443
|
-
context: {
|
|
429
|
+
suggestedFix: "Extend the expiration date or create a new discount.",
|
|
430
|
+
context: { name: attrs.name, expiresAt: attrs.expires_at }
|
|
444
431
|
}
|
|
445
432
|
)
|
|
446
433
|
);
|
|
447
434
|
}
|
|
448
|
-
if (
|
|
435
|
+
if (attrs.starts_at && new Date(attrs.starts_at) > now) {
|
|
449
436
|
issues.push(
|
|
450
437
|
issue(
|
|
451
|
-
ISSUE_CODES.
|
|
438
|
+
ISSUE_CODES.DISCOUNT_NOT_STARTED,
|
|
452
439
|
"warning",
|
|
453
|
-
"
|
|
454
|
-
{
|
|
440
|
+
`Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
|
|
441
|
+
{
|
|
442
|
+
suggestedFix: "Wait for the start date or adjust it in the dashboard.",
|
|
443
|
+
context: { name: attrs.name, startsAt: attrs.starts_at }
|
|
444
|
+
}
|
|
455
445
|
)
|
|
456
446
|
);
|
|
457
447
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
"
|
|
466
|
-
{
|
|
467
|
-
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
issues.push(
|
|
471
|
-
issue(
|
|
472
|
-
ISSUE_CODES.VARIANT_UNPUBLISHED,
|
|
473
|
-
"error",
|
|
474
|
-
"Product has variants but none are published.",
|
|
475
|
-
{ suggestedFix: "Publish at least one variant." }
|
|
476
|
-
)
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
} catch (err) {
|
|
480
|
-
const message = err instanceof Error ? err.message : "Unknown error fetching variants";
|
|
481
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
|
|
448
|
+
if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
|
|
449
|
+
issues.push(
|
|
450
|
+
issue(
|
|
451
|
+
ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
|
|
452
|
+
"warning",
|
|
453
|
+
`Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
|
|
454
|
+
{
|
|
455
|
+
suggestedFix: "Increase max_redemptions or disable the redemption limit.",
|
|
456
|
+
context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
);
|
|
482
460
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
461
|
+
if (attrs.amount <= 0) {
|
|
462
|
+
issues.push(
|
|
463
|
+
issue(
|
|
464
|
+
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
465
|
+
"error",
|
|
466
|
+
`Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
|
|
467
|
+
{
|
|
468
|
+
suggestedFix: "Set a positive discount amount in the dashboard.",
|
|
469
|
+
context: { name: attrs.name, amount: attrs.amount }
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
} else if (attrs.amount_type === "percent" && attrs.amount > 100) {
|
|
474
|
+
issues.push(
|
|
475
|
+
issue(
|
|
476
|
+
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
477
|
+
"error",
|
|
478
|
+
`Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
|
|
479
|
+
{
|
|
480
|
+
suggestedFix: "Set the discount to 100% or less.",
|
|
481
|
+
context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
return issues;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
// src/
|
|
490
|
-
function
|
|
491
|
-
return
|
|
492
|
-
}
|
|
493
|
-
function normalizeWebhookUrl(raw) {
|
|
494
|
-
return raw.replace(/\/+$/, "").toLowerCase();
|
|
489
|
+
// src/resources/licenseKeys.ts
|
|
490
|
+
async function getLicenseKey(http, licenseKeyId) {
|
|
491
|
+
return http.getResource(`/v1/license-keys/${licenseKeyId}`);
|
|
495
492
|
}
|
|
496
493
|
|
|
497
|
-
// src/
|
|
494
|
+
// src/validate/licenseKey.ts
|
|
495
|
+
async function validateLicenseKey(http, mode, options) {
|
|
496
|
+
const issues = [];
|
|
497
|
+
const fetched = await probeFetch(() => getLicenseKey(http, options.licenseKeyId), {
|
|
498
|
+
notFoundCode: ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
|
|
499
|
+
notFoundMessage: `License key ${options.licenseKeyId} not found.`,
|
|
500
|
+
notFoundFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
501
|
+
notFoundContext: { licenseKeyId: String(options.licenseKeyId) }
|
|
502
|
+
});
|
|
503
|
+
if (!fetched.ok) {
|
|
504
|
+
issues.push(fetched.issue);
|
|
505
|
+
return buildResult("licenseKey", mode, issues, void 0, {
|
|
506
|
+
label: `license key ${options.licenseKeyId}`,
|
|
507
|
+
id: String(options.licenseKeyId)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const attrs = fetched.resource.attributes;
|
|
511
|
+
issues.push(...checkLicenseKey(attrs, { storeId: options.storeId }));
|
|
512
|
+
return buildResult("licenseKey", mode, issues, attrs, {
|
|
513
|
+
label: attrs.key_short,
|
|
514
|
+
id: String(options.licenseKeyId)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function checkLicenseKey(attrs, options) {
|
|
518
|
+
const issues = [];
|
|
519
|
+
const mismatch = checkStoreOwnership({
|
|
520
|
+
expectedStoreId: options.storeId,
|
|
521
|
+
actualStoreId: attrs.store_id,
|
|
522
|
+
code: ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
|
|
523
|
+
label: "License key",
|
|
524
|
+
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores."
|
|
525
|
+
});
|
|
526
|
+
if (mismatch) issues.push(mismatch);
|
|
527
|
+
if (attrs.disabled) {
|
|
528
|
+
issues.push(
|
|
529
|
+
issue(
|
|
530
|
+
ISSUE_CODES.LICENSE_KEY_DISABLED,
|
|
531
|
+
"error",
|
|
532
|
+
`License key ${attrs.key_short} is disabled.`,
|
|
533
|
+
{
|
|
534
|
+
suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
|
|
535
|
+
context: { keyShort: attrs.key_short }
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
|
|
541
|
+
issues.push(
|
|
542
|
+
issue(
|
|
543
|
+
ISSUE_CODES.LICENSE_KEY_EXPIRED,
|
|
544
|
+
"error",
|
|
545
|
+
`License key ${attrs.key_short} expired at ${attrs.expires_at}.`,
|
|
546
|
+
{
|
|
547
|
+
suggestedFix: "Extend the expiration date or issue a new license key.",
|
|
548
|
+
context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
|
|
554
|
+
issues.push(
|
|
555
|
+
issue(
|
|
556
|
+
ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
|
|
557
|
+
"warning",
|
|
558
|
+
`License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
|
|
559
|
+
{
|
|
560
|
+
suggestedFix: "Increase the activation limit or deactivate unused instances.",
|
|
561
|
+
context: {
|
|
562
|
+
keyShort: attrs.key_short,
|
|
563
|
+
instancesCount: attrs.instances_count,
|
|
564
|
+
activationLimit: attrs.activation_limit
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return issues;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/resources/products.ts
|
|
574
|
+
async function getProduct(http, productId) {
|
|
575
|
+
return http.getResource(`/v1/products/${productId}`);
|
|
576
|
+
}
|
|
577
|
+
async function listProducts(http, storeId) {
|
|
578
|
+
return http.paginate("/v1/products", {
|
|
579
|
+
"filter[store_id]": String(storeId)
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/resources/variants.ts
|
|
584
|
+
async function getVariant(http, variantId) {
|
|
585
|
+
return http.getResource(`/v1/variants/${variantId}`);
|
|
586
|
+
}
|
|
587
|
+
async function listVariantsForProduct(http, productId) {
|
|
588
|
+
return http.paginate("/v1/variants", {
|
|
589
|
+
"filter[product_id]": String(productId)
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/validate/product.ts
|
|
594
|
+
async function validateProduct(http, mode, options) {
|
|
595
|
+
const issues = [];
|
|
596
|
+
const fetched = await probeFetch(() => getProduct(http, options.productId), {
|
|
597
|
+
notFoundCode: ISSUE_CODES.PRODUCT_NOT_FOUND,
|
|
598
|
+
notFoundMessage: `Product ${options.productId} not found.`,
|
|
599
|
+
notFoundFix: "Verify the product ID in the Lemon Squeezy dashboard.",
|
|
600
|
+
notFoundContext: { productId: String(options.productId) }
|
|
601
|
+
});
|
|
602
|
+
if (!fetched.ok) {
|
|
603
|
+
issues.push(fetched.issue);
|
|
604
|
+
return buildResult("product", mode, issues, void 0, {
|
|
605
|
+
label: `product ${options.productId}`,
|
|
606
|
+
id: String(options.productId)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const attrs = fetched.resource.attributes;
|
|
610
|
+
issues.push(...checkProduct(attrs, { expectedStoreId: options.expectedStoreId }));
|
|
611
|
+
try {
|
|
612
|
+
const variants = await listVariantsForProduct(http, options.productId);
|
|
613
|
+
if (variants.length === 0) {
|
|
614
|
+
issues.push(
|
|
615
|
+
issue(
|
|
616
|
+
ISSUE_CODES.VARIANT_MISSING,
|
|
617
|
+
"error",
|
|
618
|
+
"Product has no variants. Customers cannot purchase it.",
|
|
619
|
+
{ suggestedFix: "Add at least one variant in the product configuration." }
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
} else if (!variants.some((variant) => variant.attributes.status === "published")) {
|
|
623
|
+
issues.push(
|
|
624
|
+
issue(
|
|
625
|
+
ISSUE_CODES.VARIANT_UNPUBLISHED,
|
|
626
|
+
"error",
|
|
627
|
+
"Product has variants but none are published.",
|
|
628
|
+
{ suggestedFix: "Publish at least one variant." }
|
|
629
|
+
)
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
} catch (err) {
|
|
633
|
+
const message = err instanceof Error ? err.message : "Unknown error fetching variants";
|
|
634
|
+
issues.push(issue(ISSUE_CODES.UNKNOWN, "warning", message));
|
|
635
|
+
}
|
|
636
|
+
return buildResult("product", mode, issues, attrs, {
|
|
637
|
+
label: attrs.name,
|
|
638
|
+
id: String(options.productId)
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function checkProduct(attrs, options = {}) {
|
|
642
|
+
const issues = [];
|
|
643
|
+
if (options.expectedStoreId !== void 0) {
|
|
644
|
+
const mismatch = checkStoreOwnership({
|
|
645
|
+
expectedStoreId: options.expectedStoreId,
|
|
646
|
+
actualStoreId: attrs.store_id,
|
|
647
|
+
code: ISSUE_CODES.PRODUCT_WRONG_STORE,
|
|
648
|
+
label: "Product",
|
|
649
|
+
suggestedFix: "Either use the correct store ID or the correct product ID \u2014 IDs should not cross stores."
|
|
650
|
+
});
|
|
651
|
+
if (mismatch) issues.push(mismatch);
|
|
652
|
+
}
|
|
653
|
+
if (attrs.status !== "published") {
|
|
654
|
+
issues.push(
|
|
655
|
+
issue(
|
|
656
|
+
ISSUE_CODES.PRODUCT_UNPUBLISHED,
|
|
657
|
+
"error",
|
|
658
|
+
`Product is in "${attrs.status}" state, not "published".`,
|
|
659
|
+
{
|
|
660
|
+
suggestedFix: "Publish the product in the Lemon Squeezy dashboard before selling.",
|
|
661
|
+
context: { status: attrs.status }
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (!attrs.buy_now_url) {
|
|
667
|
+
issues.push(
|
|
668
|
+
issue(
|
|
669
|
+
ISSUE_CODES.PRODUCT_NO_BUY_URL,
|
|
670
|
+
"warning",
|
|
671
|
+
"Product has no buy-now URL. Hosted checkout may be disabled.",
|
|
672
|
+
{ suggestedFix: "Enable buy-now in product settings, or use a custom checkout flow." }
|
|
673
|
+
)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return issues;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/validate/store.ts
|
|
680
|
+
async function validateStore(http, mode, storeId) {
|
|
681
|
+
const issues = [];
|
|
682
|
+
const fetched = await probeFetch(() => getStore(http, storeId), {
|
|
683
|
+
notFoundCode: ISSUE_CODES.STORE_NOT_FOUND,
|
|
684
|
+
notFoundMessage: `Store ${storeId} not found with the current API key.`,
|
|
685
|
+
notFoundFix: "Check the store ID and confirm the API key belongs to the account that owns it.",
|
|
686
|
+
notFoundContext: { storeId: String(storeId) }
|
|
687
|
+
});
|
|
688
|
+
if (!fetched.ok) {
|
|
689
|
+
issues.push(fetched.issue);
|
|
690
|
+
return buildResult("store", mode, issues, void 0, {
|
|
691
|
+
label: `store ${storeId}`,
|
|
692
|
+
id: String(storeId)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
return buildResult("store", mode, issues, fetched.resource.attributes, {
|
|
696
|
+
label: fetched.resource.attributes.name,
|
|
697
|
+
id: String(storeId)
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/validate/subscriptionPlan.ts
|
|
702
|
+
var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
703
|
+
async function validateSubscriptionPlan(http, mode, options) {
|
|
704
|
+
const issues = [];
|
|
705
|
+
const fetched = await probeFetch(
|
|
706
|
+
() => getVariant(http, options.variantId),
|
|
707
|
+
{
|
|
708
|
+
notFoundCode: ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
709
|
+
notFoundMessage: `Variant ${options.variantId} not found.`,
|
|
710
|
+
notFoundFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
711
|
+
notFoundContext: { variantId: String(options.variantId) }
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
if (!fetched.ok) {
|
|
715
|
+
issues.push(fetched.issue);
|
|
716
|
+
return buildResult("subscriptionPlan", mode, issues, void 0, {
|
|
717
|
+
label: `variant ${options.variantId}`,
|
|
718
|
+
id: String(options.variantId)
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const variant = fetched.resource;
|
|
722
|
+
const attrs = variant.attributes;
|
|
723
|
+
issues.push(...checkSubscriptionPlan(attrs, { variantId: options.variantId }));
|
|
724
|
+
try {
|
|
725
|
+
const product = await getProduct(http, attrs.product_id);
|
|
726
|
+
const mismatch = checkStoreOwnership({
|
|
727
|
+
expectedStoreId: options.storeId,
|
|
728
|
+
actualStoreId: product.attributes.store_id,
|
|
729
|
+
code: ISSUE_CODES.PLAN_STORE_MISMATCH,
|
|
730
|
+
label: `Subscription variant (via product ${attrs.product_id})`,
|
|
731
|
+
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
732
|
+
extraContext: { productId: String(attrs.product_id) }
|
|
733
|
+
});
|
|
734
|
+
if (mismatch) issues.push(mismatch);
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
const summary = {
|
|
738
|
+
variantId: variant.id,
|
|
739
|
+
interval: attrs.interval,
|
|
740
|
+
intervalCount: attrs.interval_count,
|
|
741
|
+
price: attrs.price,
|
|
742
|
+
hasFreeTrial: attrs.has_free_trial,
|
|
743
|
+
status: attrs.status
|
|
744
|
+
};
|
|
745
|
+
return buildResult("subscriptionPlan", mode, issues, summary, {
|
|
746
|
+
label: attrs.name,
|
|
747
|
+
id: String(options.variantId)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function checkSubscriptionPlan(attrs, options) {
|
|
751
|
+
const issues = [];
|
|
752
|
+
if (!attrs.is_subscription) {
|
|
753
|
+
issues.push(
|
|
754
|
+
issue(
|
|
755
|
+
ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
|
|
756
|
+
"error",
|
|
757
|
+
`Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
|
|
758
|
+
{
|
|
759
|
+
suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
|
|
760
|
+
context: { variantId: String(options.variantId) }
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
|
|
766
|
+
issues.push(
|
|
767
|
+
issue(
|
|
768
|
+
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
769
|
+
"error",
|
|
770
|
+
`Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
|
|
771
|
+
{
|
|
772
|
+
suggestedFix: "Set a valid billing interval in the variant configuration.",
|
|
773
|
+
context: { interval: attrs.interval ?? null }
|
|
774
|
+
}
|
|
775
|
+
)
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (attrs.interval_count === null || attrs.interval_count <= 0) {
|
|
779
|
+
issues.push(
|
|
780
|
+
issue(
|
|
781
|
+
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
782
|
+
"error",
|
|
783
|
+
`Subscription variant has invalid interval_count: ${attrs.interval_count}. Must be a positive integer.`,
|
|
784
|
+
{
|
|
785
|
+
suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
|
|
786
|
+
context: { intervalCount: attrs.interval_count }
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (attrs.price === 0 && attrs.is_subscription) {
|
|
792
|
+
issues.push(
|
|
793
|
+
issue(
|
|
794
|
+
ISSUE_CODES.PLAN_FREE_PRICE,
|
|
795
|
+
"warning",
|
|
796
|
+
"Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.",
|
|
797
|
+
{
|
|
798
|
+
suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
|
|
799
|
+
context: { price: attrs.price }
|
|
800
|
+
}
|
|
801
|
+
)
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
|
|
805
|
+
issues.push(
|
|
806
|
+
issue(
|
|
807
|
+
ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
|
|
808
|
+
"warning",
|
|
809
|
+
`Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
|
|
810
|
+
{
|
|
811
|
+
suggestedFix: "Set a valid trial interval and count, or disable the free trial.",
|
|
812
|
+
context: {
|
|
813
|
+
trialInterval: attrs.trial_interval ?? null,
|
|
814
|
+
trialIntervalCount: attrs.trial_interval_count ?? null
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (attrs.status === "draft") {
|
|
821
|
+
issues.push(
|
|
822
|
+
issue(
|
|
823
|
+
ISSUE_CODES.PLAN_DRAFT,
|
|
824
|
+
"warning",
|
|
825
|
+
"Subscription variant is in draft status \u2014 customers cannot subscribe.",
|
|
826
|
+
{
|
|
827
|
+
suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
|
|
828
|
+
context: { status: attrs.status }
|
|
829
|
+
}
|
|
830
|
+
)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
return issues;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/core/equality.ts
|
|
837
|
+
function sameWebhookUrl(a, b) {
|
|
838
|
+
return normalizeWebhookUrl(a) === normalizeWebhookUrl(b);
|
|
839
|
+
}
|
|
840
|
+
function normalizeWebhookUrl(raw) {
|
|
841
|
+
return raw.replace(/\/+$/, "").toLowerCase();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/resources/webhooks.ts
|
|
498
845
|
async function listWebhooksForStore(http, storeId) {
|
|
499
846
|
return http.paginate("/v1/webhooks", {
|
|
500
847
|
"filter[store_id]": String(storeId)
|
|
@@ -629,447 +976,111 @@ var ACKNOWLEDGED_CHANGELOG_ENTRIES = [
|
|
|
629
976
|
},
|
|
630
977
|
{
|
|
631
978
|
date: "2024-02-12",
|
|
632
|
-
summary: "Added Generate subscription invoice endpoint and invoice_immediately / disable_prorations parameters.",
|
|
633
|
-
handledBy: "SubscriptionItemUpdateAttributes.invoice_immediately / disable_prorations; invoice endpoint reachable via client.request()."
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
date: "2024-02-05",
|
|
637
|
-
summary: "Added tax_inclusive parameter to Order objects and Subscription invoice objects.",
|
|
638
|
-
handledBy: "OrderAttributes.tax_inclusive, SubscriptionInvoiceAttributes.tax_inclusive."
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
date: "2024-01-21",
|
|
642
|
-
summary: "Added setup_fee_enabled / setup_fee to Price objects; setup_fee* to Subscription invoice.",
|
|
643
|
-
handledBy: "PriceAttributes.setup_fee*, OrderAttributes.setup_fee*."
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
date: "2024-01-15",
|
|
647
|
-
summary: "Added unit_price_decimal to Price objects.",
|
|
648
|
-
handledBy: "PriceAttributes.unit_price_decimal."
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
date: "2024-01-05",
|
|
652
|
-
summary: "Added test_mode flag to /v1/users/me meta.",
|
|
653
|
-
handledBy: "Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode."
|
|
654
|
-
},
|
|
655
|
-
{
|
|
656
|
-
date: "2023-09-19",
|
|
657
|
-
summary: "Added customer_portal URLs to Subscription and Customer objects.",
|
|
658
|
-
handledBy: "SubscriptionUrls.customer_portal and CustomerUrls.customer_portal."
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
date: "2023-08-23",
|
|
662
|
-
summary: "Added checkout_data.variant_quantities, subscription items, prices, and usage-based billing.",
|
|
663
|
-
handledBy: "CheckoutData.variant_quantities, SubscriptionItemAttributes, UsageRecordAttributes, PriceAttributes."
|
|
664
|
-
},
|
|
665
|
-
{
|
|
666
|
-
date: "2023-08-14",
|
|
667
|
-
summary: "Added customer_id, user_name, user_email to Subscription invoice objects.",
|
|
668
|
-
handledBy: "SubscriptionInvoiceAttributes customer fields."
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
date: "2023-03-30",
|
|
672
|
-
summary: "Added receipt URL to Order objects.",
|
|
673
|
-
handledBy: "OrderUrls.receipt."
|
|
674
|
-
}
|
|
675
|
-
];
|
|
676
|
-
|
|
677
|
-
// src/validate/webhook.ts
|
|
678
|
-
async function validateWebhook(http, mode, options) {
|
|
679
|
-
const issues = [];
|
|
680
|
-
let webhooks;
|
|
681
|
-
try {
|
|
682
|
-
webhooks = await listWebhooksForStore(http, options.storeId);
|
|
683
|
-
} catch (err) {
|
|
684
|
-
if (err instanceof FreshSqueezyError) {
|
|
685
|
-
issues.push(
|
|
686
|
-
issue(ISSUE_CODES.UNKNOWN, "error", err.message, {
|
|
687
|
-
context: { status: err.status ?? null, code: err.code }
|
|
688
|
-
})
|
|
689
|
-
);
|
|
690
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
691
|
-
label: options.url,
|
|
692
|
-
url: options.url
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
696
|
-
issues.push(issue(ISSUE_CODES.UNKNOWN, "error", message));
|
|
697
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
698
|
-
label: options.url,
|
|
699
|
-
url: options.url
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
|
|
703
|
-
if (!match) {
|
|
704
|
-
issues.push(
|
|
705
|
-
issue(
|
|
706
|
-
ISSUE_CODES.WEBHOOK_NOT_FOUND,
|
|
707
|
-
"error",
|
|
708
|
-
`No webhook registered for URL ${options.url} on store ${options.storeId}.`,
|
|
709
|
-
{
|
|
710
|
-
suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
|
|
711
|
-
context: { storeId: String(options.storeId), url: options.url }
|
|
712
|
-
}
|
|
713
|
-
)
|
|
714
|
-
);
|
|
715
|
-
return buildResult("webhook", mode, issues, void 0, {
|
|
716
|
-
label: options.url,
|
|
717
|
-
url: options.url
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
const subscribed = new Set(match.attributes.events);
|
|
721
|
-
const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
722
|
-
const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
723
|
-
if (missingRecommended.length > 0) {
|
|
724
|
-
issues.push(
|
|
725
|
-
issue(
|
|
726
|
-
ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
|
|
727
|
-
"error",
|
|
728
|
-
`Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
|
|
729
|
-
{
|
|
730
|
-
suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
|
|
731
|
-
context: { missing: missingRecommended.join(",") }
|
|
732
|
-
}
|
|
733
|
-
)
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
if (missingOptional.length > 0) {
|
|
737
|
-
issues.push(
|
|
738
|
-
issue(
|
|
739
|
-
ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
|
|
740
|
-
"info",
|
|
741
|
-
`Optional events not subscribed: ${missingOptional.join(", ")}.`,
|
|
742
|
-
{ context: { missing: missingOptional.join(",") } }
|
|
743
|
-
)
|
|
744
|
-
);
|
|
745
|
-
}
|
|
746
|
-
return buildResult("webhook", mode, issues, match.attributes, {
|
|
747
|
-
label: match.attributes.url,
|
|
748
|
-
id: match.id,
|
|
749
|
-
url: match.attributes.url
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// src/resources/discounts.ts
|
|
754
|
-
async function getDiscount(http, discountId) {
|
|
755
|
-
return http.getResource(`/v1/discounts/${discountId}`);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// src/validate/discount.ts
|
|
759
|
-
async function validateDiscount(http, mode, options) {
|
|
760
|
-
const issues = [];
|
|
761
|
-
const fetched = await probeFetch(() => getDiscount(http, options.discountId), {
|
|
762
|
-
notFoundCode: ISSUE_CODES.DISCOUNT_NOT_FOUND,
|
|
763
|
-
notFoundMessage: `Discount ${options.discountId} not found.`,
|
|
764
|
-
notFoundFix: "Verify the discount ID in the Lemon Squeezy dashboard.",
|
|
765
|
-
notFoundContext: { discountId: String(options.discountId) }
|
|
766
|
-
});
|
|
767
|
-
if (!fetched.ok) {
|
|
768
|
-
issues.push(fetched.issue);
|
|
769
|
-
return buildResult("discount", mode, issues, void 0, {
|
|
770
|
-
label: `discount ${options.discountId}`,
|
|
771
|
-
id: String(options.discountId)
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
const attrs = fetched.resource.attributes;
|
|
775
|
-
const mismatch = checkStoreOwnership({
|
|
776
|
-
expectedStoreId: options.storeId,
|
|
777
|
-
actualStoreId: attrs.store_id,
|
|
778
|
-
code: ISSUE_CODES.DISCOUNT_STORE_MISMATCH,
|
|
779
|
-
label: "Discount",
|
|
780
|
-
suggestedFix: "Use the correct store ID or discount ID \u2014 discounts should not cross stores."
|
|
781
|
-
});
|
|
782
|
-
if (mismatch) issues.push(mismatch);
|
|
783
|
-
if (attrs.status === "draft") {
|
|
784
|
-
issues.push(
|
|
785
|
-
issue(
|
|
786
|
-
ISSUE_CODES.DISCOUNT_DRAFT,
|
|
787
|
-
"warning",
|
|
788
|
-
`Discount "${attrs.name}" is in draft status \u2014 customers cannot redeem it.`,
|
|
789
|
-
{
|
|
790
|
-
suggestedFix: "Publish the discount in the Lemon Squeezy dashboard before sharing the code.",
|
|
791
|
-
context: { name: attrs.name, code: attrs.code }
|
|
792
|
-
}
|
|
793
|
-
)
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
const now = /* @__PURE__ */ new Date();
|
|
797
|
-
if (attrs.expires_at && new Date(attrs.expires_at) < now) {
|
|
798
|
-
issues.push(
|
|
799
|
-
issue(
|
|
800
|
-
ISSUE_CODES.DISCOUNT_EXPIRED,
|
|
801
|
-
"error",
|
|
802
|
-
`Discount "${attrs.name}" expired at ${attrs.expires_at}.`,
|
|
803
|
-
{
|
|
804
|
-
suggestedFix: "Extend the expiration date or create a new discount.",
|
|
805
|
-
context: { name: attrs.name, expiresAt: attrs.expires_at }
|
|
806
|
-
}
|
|
807
|
-
)
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
if (attrs.starts_at && new Date(attrs.starts_at) > now) {
|
|
811
|
-
issues.push(
|
|
812
|
-
issue(
|
|
813
|
-
ISSUE_CODES.DISCOUNT_NOT_STARTED,
|
|
814
|
-
"warning",
|
|
815
|
-
`Discount "${attrs.name}" starts at ${attrs.starts_at} \u2014 not yet active.`,
|
|
816
|
-
{
|
|
817
|
-
suggestedFix: "Wait for the start date or adjust it in the dashboard.",
|
|
818
|
-
context: { name: attrs.name, startsAt: attrs.starts_at }
|
|
819
|
-
}
|
|
820
|
-
)
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
if (attrs.is_limited_redemptions && attrs.max_redemptions <= 0) {
|
|
824
|
-
issues.push(
|
|
825
|
-
issue(
|
|
826
|
-
ISSUE_CODES.DISCOUNT_REDEMPTIONS_EXHAUSTED,
|
|
827
|
-
"warning",
|
|
828
|
-
`Discount "${attrs.name}" has limited redemptions with max_redemptions \u2264 0.`,
|
|
829
|
-
{
|
|
830
|
-
suggestedFix: "Increase max_redemptions or disable the redemption limit.",
|
|
831
|
-
context: { name: attrs.name, maxRedemptions: attrs.max_redemptions }
|
|
832
|
-
}
|
|
833
|
-
)
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
if (attrs.amount <= 0) {
|
|
837
|
-
issues.push(
|
|
838
|
-
issue(
|
|
839
|
-
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
840
|
-
"error",
|
|
841
|
-
`Discount "${attrs.name}" has amount ${attrs.amount} \u2014 must be positive.`,
|
|
842
|
-
{
|
|
843
|
-
suggestedFix: "Set a positive discount amount in the dashboard.",
|
|
844
|
-
context: { name: attrs.name, amount: attrs.amount }
|
|
845
|
-
}
|
|
846
|
-
)
|
|
847
|
-
);
|
|
848
|
-
} else if (attrs.amount_type === "percent" && attrs.amount > 100) {
|
|
849
|
-
issues.push(
|
|
850
|
-
issue(
|
|
851
|
-
ISSUE_CODES.DISCOUNT_INVALID_AMOUNT,
|
|
852
|
-
"error",
|
|
853
|
-
`Discount "${attrs.name}" is ${attrs.amount}% \u2014 percent discounts cannot exceed 100%.`,
|
|
854
|
-
{
|
|
855
|
-
suggestedFix: "Set the discount to 100% or less.",
|
|
856
|
-
context: { name: attrs.name, amount: attrs.amount, amountType: attrs.amount_type }
|
|
857
|
-
}
|
|
858
|
-
)
|
|
859
|
-
);
|
|
979
|
+
summary: "Added Generate subscription invoice endpoint and invoice_immediately / disable_prorations parameters.",
|
|
980
|
+
handledBy: "SubscriptionItemUpdateAttributes.invoice_immediately / disable_prorations; invoice endpoint reachable via client.request()."
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
date: "2024-02-05",
|
|
984
|
+
summary: "Added tax_inclusive parameter to Order objects and Subscription invoice objects.",
|
|
985
|
+
handledBy: "OrderAttributes.tax_inclusive, SubscriptionInvoiceAttributes.tax_inclusive."
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
date: "2024-01-21",
|
|
989
|
+
summary: "Added setup_fee_enabled / setup_fee to Price objects; setup_fee* to Subscription invoice.",
|
|
990
|
+
handledBy: "PriceAttributes.setup_fee*, OrderAttributes.setup_fee*."
|
|
991
|
+
},
|
|
992
|
+
{
|
|
993
|
+
date: "2024-01-15",
|
|
994
|
+
summary: "Added unit_price_decimal to Price objects.",
|
|
995
|
+
handledBy: "PriceAttributes.unit_price_decimal."
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
date: "2024-01-05",
|
|
999
|
+
summary: "Added test_mode flag to /v1/users/me meta.",
|
|
1000
|
+
handledBy: "Read in validateConnection to emit MODE_MISMATCH when the key's true mode differs from the caller's declared mode."
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
date: "2023-09-19",
|
|
1004
|
+
summary: "Added customer_portal URLs to Subscription and Customer objects.",
|
|
1005
|
+
handledBy: "SubscriptionUrls.customer_portal and CustomerUrls.customer_portal."
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
date: "2023-08-23",
|
|
1009
|
+
summary: "Added checkout_data.variant_quantities, subscription items, prices, and usage-based billing.",
|
|
1010
|
+
handledBy: "CheckoutData.variant_quantities, SubscriptionItemAttributes, UsageRecordAttributes, PriceAttributes."
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
date: "2023-08-14",
|
|
1014
|
+
summary: "Added customer_id, user_name, user_email to Subscription invoice objects.",
|
|
1015
|
+
handledBy: "SubscriptionInvoiceAttributes customer fields."
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
date: "2023-03-30",
|
|
1019
|
+
summary: "Added receipt URL to Order objects.",
|
|
1020
|
+
handledBy: "OrderUrls.receipt."
|
|
860
1021
|
}
|
|
861
|
-
|
|
862
|
-
label: `${attrs.name} (${attrs.code})`,
|
|
863
|
-
id: String(options.discountId)
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// src/resources/licenseKeys.ts
|
|
868
|
-
async function getLicenseKey(http, licenseKeyId) {
|
|
869
|
-
return http.getResource(`/v1/license-keys/${licenseKeyId}`);
|
|
870
|
-
}
|
|
1022
|
+
];
|
|
871
1023
|
|
|
872
|
-
// src/validate/
|
|
873
|
-
async function
|
|
1024
|
+
// src/validate/webhook.ts
|
|
1025
|
+
async function validateWebhook(http, mode, options) {
|
|
874
1026
|
const issues = [];
|
|
875
|
-
const fetched = await
|
|
876
|
-
notFoundCode: ISSUE_CODES.LICENSE_KEY_NOT_FOUND,
|
|
877
|
-
notFoundMessage: `License key ${options.licenseKeyId} not found.`,
|
|
878
|
-
notFoundFix: "Verify the license key ID in the Lemon Squeezy dashboard.",
|
|
879
|
-
notFoundContext: { licenseKeyId: String(options.licenseKeyId) }
|
|
880
|
-
});
|
|
1027
|
+
const fetched = await probeCollection(() => listWebhooksForStore(http, options.storeId));
|
|
881
1028
|
if (!fetched.ok) {
|
|
882
1029
|
issues.push(fetched.issue);
|
|
883
|
-
return buildResult("
|
|
884
|
-
label:
|
|
885
|
-
|
|
1030
|
+
return buildResult("webhook", mode, issues, void 0, {
|
|
1031
|
+
label: options.url,
|
|
1032
|
+
url: options.url
|
|
886
1033
|
});
|
|
887
1034
|
}
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
actualStoreId: attrs.store_id,
|
|
892
|
-
code: ISSUE_CODES.LICENSE_KEY_STORE_MISMATCH,
|
|
893
|
-
label: "License key",
|
|
894
|
-
suggestedFix: "Use the correct store ID or license key ID \u2014 keys should not cross stores."
|
|
895
|
-
});
|
|
896
|
-
if (mismatch) issues.push(mismatch);
|
|
897
|
-
if (attrs.disabled) {
|
|
898
|
-
issues.push(
|
|
899
|
-
issue(
|
|
900
|
-
ISSUE_CODES.LICENSE_KEY_DISABLED,
|
|
901
|
-
"error",
|
|
902
|
-
`License key ${attrs.key_short} is disabled.`,
|
|
903
|
-
{
|
|
904
|
-
suggestedFix: "Re-enable the license key in the Lemon Squeezy dashboard.",
|
|
905
|
-
context: { keyShort: attrs.key_short }
|
|
906
|
-
}
|
|
907
|
-
)
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
if (attrs.expires_at && new Date(attrs.expires_at) < /* @__PURE__ */ new Date()) {
|
|
1035
|
+
const webhooks = fetched.resource;
|
|
1036
|
+
const match = webhooks.find((webhook) => sameWebhookUrl(webhook.attributes.url, options.url));
|
|
1037
|
+
if (!match) {
|
|
911
1038
|
issues.push(
|
|
912
1039
|
issue(
|
|
913
|
-
ISSUE_CODES.
|
|
1040
|
+
ISSUE_CODES.WEBHOOK_NOT_FOUND,
|
|
914
1041
|
"error",
|
|
915
|
-
`
|
|
916
|
-
{
|
|
917
|
-
suggestedFix: "Extend the expiration date or issue a new license key.",
|
|
918
|
-
context: { keyShort: attrs.key_short, expiresAt: attrs.expires_at }
|
|
919
|
-
}
|
|
920
|
-
)
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
if (attrs.activation_limit !== null && attrs.instances_count >= attrs.activation_limit) {
|
|
924
|
-
issues.push(
|
|
925
|
-
issue(
|
|
926
|
-
ISSUE_CODES.LICENSE_KEY_AT_ACTIVATION_LIMIT,
|
|
927
|
-
"warning",
|
|
928
|
-
`License key ${attrs.key_short} has reached its activation limit (${attrs.instances_count}/${attrs.activation_limit}).`,
|
|
1042
|
+
`No webhook registered for URL ${options.url} on store ${options.storeId}.`,
|
|
929
1043
|
{
|
|
930
|
-
suggestedFix: "
|
|
931
|
-
context: {
|
|
932
|
-
keyShort: attrs.key_short,
|
|
933
|
-
instancesCount: attrs.instances_count,
|
|
934
|
-
activationLimit: attrs.activation_limit
|
|
935
|
-
}
|
|
1044
|
+
suggestedFix: "Register the webhook in Lemon Squeezy (Settings \u2192 Webhooks) and subscribe to the recommended events.",
|
|
1045
|
+
context: { storeId: String(options.storeId), url: options.url }
|
|
936
1046
|
}
|
|
937
1047
|
)
|
|
938
1048
|
);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
id: String(options.licenseKeyId)
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// src/validate/subscriptionPlan.ts
|
|
947
|
-
var VALID_INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
|
|
948
|
-
async function validateSubscriptionPlan(http, mode, options) {
|
|
949
|
-
const issues = [];
|
|
950
|
-
const fetched = await probeFetch(
|
|
951
|
-
() => getVariant(http, options.variantId),
|
|
952
|
-
{
|
|
953
|
-
notFoundCode: ISSUE_CODES.PLAN_VARIANT_NOT_FOUND,
|
|
954
|
-
notFoundMessage: `Variant ${options.variantId} not found.`,
|
|
955
|
-
notFoundFix: "Verify the variant ID in the Lemon Squeezy dashboard.",
|
|
956
|
-
notFoundContext: { variantId: String(options.variantId) }
|
|
957
|
-
}
|
|
958
|
-
);
|
|
959
|
-
if (!fetched.ok) {
|
|
960
|
-
issues.push(fetched.issue);
|
|
961
|
-
return buildResult("subscriptionPlan", mode, issues, void 0, {
|
|
962
|
-
label: `variant ${options.variantId}`,
|
|
963
|
-
id: String(options.variantId)
|
|
1049
|
+
return buildResult("webhook", mode, issues, void 0, {
|
|
1050
|
+
label: options.url,
|
|
1051
|
+
url: options.url
|
|
964
1052
|
});
|
|
965
1053
|
}
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
issue(
|
|
971
|
-
ISSUE_CODES.PLAN_NOT_SUBSCRIPTION,
|
|
972
|
-
"error",
|
|
973
|
-
`Variant ${options.variantId} is not a subscription variant (is_subscription is false).`,
|
|
974
|
-
{
|
|
975
|
-
suggestedFix: "Use a variant that has subscription billing enabled, or use the regular variant validator.",
|
|
976
|
-
context: { variantId: String(options.variantId) }
|
|
977
|
-
}
|
|
978
|
-
)
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
if (!attrs.interval || !VALID_INTERVALS.has(attrs.interval)) {
|
|
982
|
-
issues.push(
|
|
983
|
-
issue(
|
|
984
|
-
ISSUE_CODES.PLAN_INVALID_INTERVAL,
|
|
985
|
-
"error",
|
|
986
|
-
`Subscription variant has invalid interval: "${attrs.interval ?? "missing"}". Expected one of: day, week, month, year.`,
|
|
987
|
-
{
|
|
988
|
-
suggestedFix: "Set a valid billing interval in the variant configuration.",
|
|
989
|
-
context: { interval: attrs.interval ?? null }
|
|
990
|
-
}
|
|
991
|
-
)
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
if (attrs.interval_count === null || attrs.interval_count <= 0) {
|
|
1054
|
+
const subscribed = new Set(match.attributes.events);
|
|
1055
|
+
const missingRecommended = RECOMMENDED_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
1056
|
+
const missingOptional = OPTIONAL_WEBHOOK_EVENTS.filter((event) => !subscribed.has(event));
|
|
1057
|
+
if (missingRecommended.length > 0) {
|
|
995
1058
|
issues.push(
|
|
996
1059
|
issue(
|
|
997
|
-
ISSUE_CODES.
|
|
1060
|
+
ISSUE_CODES.WEBHOOK_EVENTS_MISSING,
|
|
998
1061
|
"error",
|
|
999
|
-
`
|
|
1000
|
-
{
|
|
1001
|
-
suggestedFix: "Set interval_count to a positive value (e.g. 1 for monthly, 2 for biweekly).",
|
|
1002
|
-
context: { intervalCount: attrs.interval_count }
|
|
1003
|
-
}
|
|
1004
|
-
)
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
if (attrs.price === 0 && attrs.is_subscription) {
|
|
1008
|
-
issues.push(
|
|
1009
|
-
issue(
|
|
1010
|
-
ISSUE_CODES.PLAN_FREE_PRICE,
|
|
1011
|
-
"warning",
|
|
1012
|
-
`Subscription variant has a price of 0 \u2014 this is almost always a misconfiguration for paid plans.`,
|
|
1013
|
-
{
|
|
1014
|
-
suggestedFix: "Set the variant price to the intended amount in cents, or confirm this is intentionally free.",
|
|
1015
|
-
context: { price: attrs.price }
|
|
1016
|
-
}
|
|
1017
|
-
)
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
if (attrs.has_free_trial && (!attrs.trial_interval || (attrs.trial_interval_count ?? 0) <= 0)) {
|
|
1021
|
-
issues.push(
|
|
1022
|
-
issue(
|
|
1023
|
-
ISSUE_CODES.PLAN_TRIAL_INCONSISTENT,
|
|
1024
|
-
"warning",
|
|
1025
|
-
`Subscription variant has free trial enabled but trial interval is misconfigured (interval: "${attrs.trial_interval ?? "missing"}", count: ${attrs.trial_interval_count ?? 0}).`,
|
|
1062
|
+
`Webhook is missing recommended events: ${missingRecommended.join(", ")}.`,
|
|
1026
1063
|
{
|
|
1027
|
-
suggestedFix: "
|
|
1028
|
-
context: {
|
|
1029
|
-
trialInterval: attrs.trial_interval ?? null,
|
|
1030
|
-
trialIntervalCount: attrs.trial_interval_count ?? null
|
|
1031
|
-
}
|
|
1064
|
+
suggestedFix: "Subscribe to all recommended events so the integration survives plan changes and refunds.",
|
|
1065
|
+
context: { missing: missingRecommended.join(",") }
|
|
1032
1066
|
}
|
|
1033
1067
|
)
|
|
1034
1068
|
);
|
|
1035
1069
|
}
|
|
1036
|
-
if (
|
|
1070
|
+
if (missingOptional.length > 0) {
|
|
1037
1071
|
issues.push(
|
|
1038
1072
|
issue(
|
|
1039
|
-
ISSUE_CODES.
|
|
1040
|
-
"
|
|
1041
|
-
`
|
|
1042
|
-
{
|
|
1043
|
-
suggestedFix: "Publish the variant in the Lemon Squeezy dashboard.",
|
|
1044
|
-
context: { status: attrs.status }
|
|
1045
|
-
}
|
|
1073
|
+
ISSUE_CODES.WEBHOOK_OPTIONAL_EVENTS,
|
|
1074
|
+
"info",
|
|
1075
|
+
`Optional events not subscribed: ${missingOptional.join(", ")}.`,
|
|
1076
|
+
{ context: { missing: missingOptional.join(",") } }
|
|
1046
1077
|
)
|
|
1047
1078
|
);
|
|
1048
1079
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
actualStoreId: product.attributes.store_id,
|
|
1054
|
-
code: ISSUE_CODES.PLAN_STORE_MISMATCH,
|
|
1055
|
-
label: `Subscription variant (via product ${attrs.product_id})`,
|
|
1056
|
-
suggestedFix: "Use the correct store ID or variant ID \u2014 plans should not cross stores.",
|
|
1057
|
-
extraContext: { productId: String(attrs.product_id) }
|
|
1058
|
-
});
|
|
1059
|
-
if (mismatch) issues.push(mismatch);
|
|
1060
|
-
} catch {
|
|
1061
|
-
}
|
|
1062
|
-
const summary = {
|
|
1063
|
-
variantId: variant.id,
|
|
1064
|
-
interval: attrs.interval,
|
|
1065
|
-
intervalCount: attrs.interval_count,
|
|
1066
|
-
price: attrs.price,
|
|
1067
|
-
hasFreeTrial: attrs.has_free_trial,
|
|
1068
|
-
status: attrs.status
|
|
1069
|
-
};
|
|
1070
|
-
return buildResult("subscriptionPlan", mode, issues, summary, {
|
|
1071
|
-
label: attrs.name,
|
|
1072
|
-
id: String(options.variantId)
|
|
1080
|
+
return buildResult("webhook", mode, issues, match.attributes, {
|
|
1081
|
+
label: match.attributes.url,
|
|
1082
|
+
id: match.id,
|
|
1083
|
+
url: match.attributes.url
|
|
1073
1084
|
});
|
|
1074
1085
|
}
|
|
1075
1086
|
|
|
@@ -1349,6 +1360,10 @@ export {
|
|
|
1349
1360
|
SUPPORTED_RESOURCES,
|
|
1350
1361
|
buildResult,
|
|
1351
1362
|
buildSubscriptionItemUpdateBody,
|
|
1363
|
+
checkDiscount,
|
|
1364
|
+
checkLicenseKey,
|
|
1365
|
+
checkProduct,
|
|
1366
|
+
checkSubscriptionPlan,
|
|
1352
1367
|
createFreshSqueezy,
|
|
1353
1368
|
doctor,
|
|
1354
1369
|
fetchActualMode,
|
|
@@ -1390,7 +1405,11 @@ export {
|
|
|
1390
1405
|
sameWebhookUrl,
|
|
1391
1406
|
userResource,
|
|
1392
1407
|
validateConnection,
|
|
1408
|
+
validateDiscount,
|
|
1409
|
+
validateLicenseKey,
|
|
1393
1410
|
validateProduct,
|
|
1411
|
+
validateStore,
|
|
1412
|
+
validateSubscriptionPlan,
|
|
1394
1413
|
validateWebhook
|
|
1395
1414
|
};
|
|
1396
1415
|
//# sourceMappingURL=index.js.map
|