@layr-labs/ecloud-cli 1.0.0-devep1 → 1.0.0-devep2
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/VERSION +2 -2
- package/dist/commands/billing/__tests__/status.test.js +4 -23
- package/dist/commands/billing/__tests__/status.test.js.map +1 -1
- package/dist/commands/billing/__tests__/top-up.test.js +273 -89
- package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
- package/dist/commands/billing/list-cards.js +409 -0
- package/dist/commands/billing/list-cards.js.map +1 -0
- package/dist/commands/billing/status.js +4 -23
- package/dist/commands/billing/status.js.map +1 -1
- package/dist/commands/billing/top-up.js +151 -79
- package/dist/commands/billing/top-up.js.map +1 -1
- package/dist/commands/compute/app/deploy.js +1 -1
- package/dist/commands/compute/app/info.js +1 -1
- package/dist/commands/compute/app/list.js +1 -1
- package/dist/commands/compute/app/logs.js +1 -1
- package/dist/commands/compute/app/profile/set.js +1 -1
- package/dist/commands/compute/app/releases.js +1 -1
- package/dist/commands/compute/app/start.js +1 -1
- package/dist/commands/compute/app/stop.js +1 -1
- package/dist/commands/compute/app/terminate.js +1 -1
- package/dist/commands/compute/app/upgrade.js +1 -1
- package/dist/commands/compute/build/info.js +1 -1
- package/dist/commands/compute/build/list.js +1 -1
- package/dist/commands/compute/build/logs.js +1 -1
- package/dist/commands/compute/build/status.js +1 -1
- package/dist/commands/compute/build/submit.js +1 -1
- package/dist/commands/compute/build/verify.js +1 -1
- package/dist/commands/compute/undelegate.js +1 -1
- package/dist/hooks/init/__tests__/version-check.test.js +1 -1
- package/dist/hooks/init/version-check.js +1 -1
- package/package.json +2 -2
|
@@ -319,7 +319,8 @@ async function createBillingClient(flags) {
|
|
|
319
319
|
// src/commands/billing/top-up.ts
|
|
320
320
|
import { formatUnits } from "viem";
|
|
321
321
|
import chalk2 from "chalk";
|
|
322
|
-
import { input as input2 } from "@inquirer/prompts";
|
|
322
|
+
import { input as input2, select as select2 } from "@inquirer/prompts";
|
|
323
|
+
import open from "open";
|
|
323
324
|
|
|
324
325
|
// src/telemetry.ts
|
|
325
326
|
import {
|
|
@@ -383,12 +384,22 @@ async function withTelemetry(command, action) {
|
|
|
383
384
|
var POLL_INTERVAL_MS = 5e3;
|
|
384
385
|
var POLL_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
385
386
|
var BillingTopUp = class _BillingTopUp extends Command {
|
|
386
|
-
static description = "Purchase EigenCompute credits with USDC";
|
|
387
|
+
static description = "Purchase EigenCompute credits with USDC or credit card";
|
|
388
|
+
static examples = [
|
|
389
|
+
"<%= config.bin %> billing top-up",
|
|
390
|
+
"<%= config.bin %> billing top-up --method usdc --amount 50",
|
|
391
|
+
"<%= config.bin %> billing top-up --method card --amount 25"
|
|
392
|
+
];
|
|
387
393
|
static flags = {
|
|
388
394
|
...commonFlags,
|
|
395
|
+
method: Flags2.string({
|
|
396
|
+
required: false,
|
|
397
|
+
description: "Payment method: usdc (on-chain) or card (credit card)",
|
|
398
|
+
options: ["usdc", "card"]
|
|
399
|
+
}),
|
|
389
400
|
amount: Flags2.string({
|
|
390
401
|
required: false,
|
|
391
|
-
description: "Amount
|
|
402
|
+
description: "Amount to spend (USDC for on-chain, whole dollars for card)"
|
|
392
403
|
}),
|
|
393
404
|
account: Flags2.string({
|
|
394
405
|
required: false,
|
|
@@ -424,101 +435,162 @@ ${chalk2.bold("Purchase EigenCompute credits")}`);
|
|
|
424
435
|
const remaining = status.remainingCredits ?? 0;
|
|
425
436
|
const applied = status.creditsApplied ?? 0;
|
|
426
437
|
baselineTotal = remaining + applied;
|
|
427
|
-
|
|
428
|
-
this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${status.remainingCredits.toFixed(2)}`)}`);
|
|
429
|
-
}
|
|
438
|
+
this.log(` ${chalk2.bold("Credits:")} ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
|
|
430
439
|
} catch {
|
|
431
440
|
this.debug("Could not fetch current credit balance");
|
|
432
441
|
}
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
const method = flags.method ?? await select2({
|
|
443
|
+
message: "How would you like to pay?",
|
|
444
|
+
choices: [
|
|
445
|
+
{ value: "card", name: "Credit card" },
|
|
446
|
+
{ value: "usdc", name: "USDC (on-chain)" }
|
|
447
|
+
]
|
|
448
|
+
});
|
|
449
|
+
if (method === "usdc") {
|
|
450
|
+
await this.handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal);
|
|
451
|
+
} else {
|
|
452
|
+
await this.handleCard(billing, flags, baselineTotal);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async handleUsdc(billing, flags, walletAddress, targetAccount, baselineTotal) {
|
|
457
|
+
const onChainState = await billing.getTopUpInfo();
|
|
458
|
+
const { usdcBalance, minimumPurchase } = onChainState;
|
|
459
|
+
const balanceFormatted = formatUnits(usdcBalance, 6);
|
|
460
|
+
this.log(` ${chalk2.bold("USDC:")} ${balanceFormatted} USDC`);
|
|
461
|
+
if (usdcBalance === BigInt(0)) {
|
|
462
|
+
this.log(`
|
|
439
463
|
${chalk2.yellow(" No USDC in wallet.")}`);
|
|
440
|
-
|
|
441
|
-
|
|
464
|
+
this.log(` Send USDC on Sepolia to: ${chalk2.cyan(walletAddress)}`);
|
|
465
|
+
this.log(` Then re-run: ${chalk2.cyan("ecloud billing top-up")}
|
|
442
466
|
`);
|
|
443
|
-
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const minimumFormatted = formatUnits(minimumPurchase, 6);
|
|
470
|
+
const amountStr = flags.amount ?? await input2({
|
|
471
|
+
message: `How much USDC to spend on credits? (minimum: ${minimumFormatted})`,
|
|
472
|
+
validate: (val) => {
|
|
473
|
+
const n = parseFloat(val);
|
|
474
|
+
if (isNaN(n) || n <= 0) return "Enter a positive number";
|
|
475
|
+
const raw = BigInt(Math.round(n * 1e6));
|
|
476
|
+
if (raw < minimumPurchase)
|
|
477
|
+
return `Minimum purchase is ${minimumFormatted} USDC`;
|
|
478
|
+
if (raw > usdcBalance)
|
|
479
|
+
return `Insufficient balance. You have ${balanceFormatted} USDC`;
|
|
480
|
+
return true;
|
|
444
481
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
482
|
+
});
|
|
483
|
+
const amountFloat = parseFloat(amountStr);
|
|
484
|
+
const amountRaw = BigInt(Math.round(amountFloat * 1e6));
|
|
485
|
+
if (amountRaw < minimumPurchase) {
|
|
486
|
+
this.error(`Minimum purchase is ${minimumFormatted} USDC`);
|
|
487
|
+
}
|
|
488
|
+
if (amountRaw > usdcBalance) {
|
|
489
|
+
this.error(
|
|
490
|
+
`Insufficient USDC balance. You have ${balanceFormatted} USDC but requested ${amountFloat.toFixed(2)}`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
this.log(`
|
|
494
|
+
Purchasing ${chalk2.bold(`$${amountFloat.toFixed(2)}`)} in credits...`);
|
|
495
|
+
const { txHash } = await billing.topUp({
|
|
496
|
+
amount: amountRaw,
|
|
497
|
+
account: targetAccount
|
|
498
|
+
});
|
|
499
|
+
this.log(` ${chalk2.green("\u2713")} Transaction confirmed: ${txHash}`);
|
|
500
|
+
await this.pollForCredits(billing, flags, baselineTotal, amountFloat);
|
|
501
|
+
}
|
|
502
|
+
async handleCard(billing, flags, baselineTotal) {
|
|
503
|
+
const MINIMUM_DOLLARS = 5;
|
|
504
|
+
const amountStr = flags.amount ?? await input2({
|
|
505
|
+
message: `How many dollars of credits to purchase? (minimum: $${MINIMUM_DOLLARS})`,
|
|
506
|
+
validate: (val) => {
|
|
507
|
+
const n = parseInt(val, 10);
|
|
508
|
+
if (isNaN(n) || n <= 0) return "Enter a positive whole number";
|
|
509
|
+
if (n.toString() !== val.trim()) return "Enter a whole dollar amount (no cents)";
|
|
510
|
+
if (n < MINIMUM_DOLLARS) return `Minimum purchase is $${MINIMUM_DOLLARS}`;
|
|
511
|
+
return true;
|
|
463
512
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
513
|
+
});
|
|
514
|
+
const dollars = parseInt(amountStr, 10);
|
|
515
|
+
if (isNaN(dollars) || dollars < MINIMUM_DOLLARS) {
|
|
516
|
+
this.error(`Minimum purchase is $${MINIMUM_DOLLARS}`);
|
|
517
|
+
}
|
|
518
|
+
const amountCents = dollars * 100;
|
|
519
|
+
const { paymentMethods } = await billing.getPaymentMethods();
|
|
520
|
+
let paymentMethodId;
|
|
521
|
+
if (paymentMethods.length > 0) {
|
|
522
|
+
const choices = paymentMethods.map((card) => ({
|
|
523
|
+
value: card.id,
|
|
524
|
+
name: `${card.brand.charAt(0).toUpperCase() + card.brand.slice(1)} ending in ${card.last4}`
|
|
525
|
+
}));
|
|
526
|
+
choices.push({ value: "new", name: "Add a new card" });
|
|
527
|
+
const selection = await select2({
|
|
528
|
+
message: "Which card would you like to use?",
|
|
529
|
+
choices
|
|
530
|
+
});
|
|
531
|
+
if (selection !== "new") {
|
|
532
|
+
paymentMethodId = selection;
|
|
468
533
|
}
|
|
534
|
+
}
|
|
535
|
+
this.log(`
|
|
536
|
+
Purchasing ${chalk2.bold(`$${dollars}`)} in credits...`);
|
|
537
|
+
const result = await billing.purchaseCredits(amountCents, paymentMethodId);
|
|
538
|
+
if (result.checkoutUrl) {
|
|
469
539
|
this.log(`
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
540
|
+
${chalk2.cyan(result.checkoutUrl)}`);
|
|
541
|
+
this.log(chalk2.gray(" Opening checkout in browser..."));
|
|
542
|
+
await open(result.checkoutUrl);
|
|
543
|
+
} else if (result.checkoutSessionId) {
|
|
544
|
+
this.error(
|
|
545
|
+
"Checkout session created but no URL was returned. Please contact support."
|
|
546
|
+
);
|
|
547
|
+
} else {
|
|
548
|
+
this.log(` ${chalk2.green("\u2713")} Payment submitted`);
|
|
549
|
+
}
|
|
550
|
+
await this.pollForCredits(billing, flags, baselineTotal, dollars);
|
|
551
|
+
}
|
|
552
|
+
async pollForCredits(billing, flags, baselineTotal, amountPurchased) {
|
|
553
|
+
this.log(chalk2.gray("\n Waiting for credits to appear..."));
|
|
554
|
+
const startTime = Date.now();
|
|
555
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
556
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
557
|
+
try {
|
|
558
|
+
const status = await billing.getStatus({
|
|
559
|
+
productId: flags.product
|
|
560
|
+
});
|
|
561
|
+
const remaining = status.remainingCredits ?? 0;
|
|
562
|
+
const applied = status.creditsApplied ?? 0;
|
|
563
|
+
const currentTotal = remaining + applied;
|
|
564
|
+
this.debug(
|
|
565
|
+
`Poll: remaining=${remaining}, applied=${applied}, total=${currentTotal}, baseline=${baselineTotal}`
|
|
566
|
+
);
|
|
567
|
+
if (baselineTotal === void 0 || currentTotal > baselineTotal) {
|
|
568
|
+
const creditsAdded = baselineTotal !== void 0 ? currentTotal - baselineTotal : void 0;
|
|
569
|
+
this.log(
|
|
570
|
+
`
|
|
571
|
+
${chalk2.green("\u2713")} Credits received: ${chalk2.cyan(`$${(creditsAdded ?? amountPurchased).toFixed(2)}`)}`
|
|
572
|
+
);
|
|
573
|
+
if (remaining > 0) {
|
|
574
|
+
this.log(` Remaining balance: ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
|
|
505
575
|
}
|
|
506
|
-
|
|
507
|
-
|
|
576
|
+
this.log();
|
|
577
|
+
return;
|
|
508
578
|
}
|
|
579
|
+
} catch {
|
|
580
|
+
this.debug("Error polling for credit balance");
|
|
509
581
|
}
|
|
510
|
-
|
|
511
|
-
|
|
582
|
+
}
|
|
583
|
+
this.log(
|
|
584
|
+
`
|
|
512
585
|
${chalk2.yellow("\u26A0")} Credits haven't appeared yet. This can take a few minutes.`
|
|
513
|
-
|
|
514
|
-
|
|
586
|
+
);
|
|
587
|
+
this.log(` ${chalk2.gray("Check your balance:")} ecloud billing status
|
|
515
588
|
`);
|
|
516
|
-
});
|
|
517
589
|
}
|
|
518
590
|
};
|
|
519
591
|
|
|
520
592
|
// src/commands/billing/__tests__/top-up.test.ts
|
|
521
|
-
import { input as input3 } from "@inquirer/prompts";
|
|
593
|
+
import { input as input3, select as select3 } from "@inquirer/prompts";
|
|
522
594
|
vi.mock("../../../client", () => ({
|
|
523
595
|
createBillingClient: vi.fn()
|
|
524
596
|
}));
|
|
@@ -526,7 +598,11 @@ vi.mock("../../../telemetry", () => ({
|
|
|
526
598
|
withTelemetry: vi.fn((_cmd, fn) => fn())
|
|
527
599
|
}));
|
|
528
600
|
vi.mock("@inquirer/prompts", () => ({
|
|
529
|
-
input: vi.fn()
|
|
601
|
+
input: vi.fn(),
|
|
602
|
+
select: vi.fn()
|
|
603
|
+
}));
|
|
604
|
+
vi.mock("open", () => ({
|
|
605
|
+
default: vi.fn()
|
|
530
606
|
}));
|
|
531
607
|
var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
|
|
532
608
|
var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
|
|
@@ -541,7 +617,9 @@ describe("ecloud billing top-up", () => {
|
|
|
541
617
|
address: WALLET_ADDRESS,
|
|
542
618
|
getStatus: vi.fn(),
|
|
543
619
|
getTopUpInfo: vi.fn(),
|
|
544
|
-
topUp: vi.fn()
|
|
620
|
+
topUp: vi.fn(),
|
|
621
|
+
getPaymentMethods: vi.fn(),
|
|
622
|
+
purchaseCredits: vi.fn()
|
|
545
623
|
};
|
|
546
624
|
createBillingClient.mockResolvedValue(mockBilling);
|
|
547
625
|
input3.mockResolvedValue("50");
|
|
@@ -586,7 +664,7 @@ describe("ecloud billing top-up", () => {
|
|
|
586
664
|
setupOnChainState();
|
|
587
665
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
588
666
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
589
|
-
const cmd = createCommand({ amount: "50" });
|
|
667
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
590
668
|
const promise = cmd.run();
|
|
591
669
|
for (let i = 0; i < 10; i++) {
|
|
592
670
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -608,7 +686,7 @@ describe("ecloud billing top-up", () => {
|
|
|
608
686
|
it("zero USDC balance: exits with fund wallet message", async () => {
|
|
609
687
|
setupOnChainState({ usdcBalance: BigInt(0) });
|
|
610
688
|
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
|
|
611
|
-
const cmd = createCommand({ amount: "50" });
|
|
689
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
612
690
|
await cmd.run();
|
|
613
691
|
const fullOutput = logOutput.join("\n");
|
|
614
692
|
expect(fullOutput).toContain("No USDC in wallet");
|
|
@@ -619,7 +697,7 @@ describe("ecloud billing top-up", () => {
|
|
|
619
697
|
it("below minimum purchase: shows error", async () => {
|
|
620
698
|
setupOnChainState({ minimumPurchase: BigInt(1e7) });
|
|
621
699
|
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
|
|
622
|
-
const cmd = createCommand({ amount: "5" });
|
|
700
|
+
const cmd = createCommand({ amount: "5", method: "usdc" });
|
|
623
701
|
await expect(cmd.run()).rejects.toThrow("Minimum purchase is 10 USDC");
|
|
624
702
|
});
|
|
625
703
|
it("--account flag: passes different address to topUp", async () => {
|
|
@@ -627,7 +705,7 @@ describe("ecloud billing top-up", () => {
|
|
|
627
705
|
setupOnChainState();
|
|
628
706
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
629
707
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
630
|
-
const cmd = createCommand({ amount: "50", account: targetAccount });
|
|
708
|
+
const cmd = createCommand({ amount: "50", method: "usdc", account: targetAccount });
|
|
631
709
|
const promise = cmd.run();
|
|
632
710
|
for (let i = 0; i < 10; i++) {
|
|
633
711
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -647,7 +725,7 @@ describe("ecloud billing top-up", () => {
|
|
|
647
725
|
subscriptionStatus: "active",
|
|
648
726
|
remainingCredits: 10
|
|
649
727
|
});
|
|
650
|
-
const cmd = createCommand({ amount: "50" });
|
|
728
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
651
729
|
const promise = cmd.run();
|
|
652
730
|
await vi.advanceTimersByTimeAsync(2e5);
|
|
653
731
|
await promise;
|
|
@@ -659,7 +737,7 @@ describe("ecloud billing top-up", () => {
|
|
|
659
737
|
setupOnChainState();
|
|
660
738
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
661
739
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "inactive" }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 100 });
|
|
662
|
-
const cmd = createCommand({ amount: "100" });
|
|
740
|
+
const cmd = createCommand({ amount: "100", method: "usdc" });
|
|
663
741
|
const promise = cmd.run();
|
|
664
742
|
for (let i = 0; i < 10; i++) {
|
|
665
743
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -671,7 +749,7 @@ describe("ecloud billing top-up", () => {
|
|
|
671
749
|
setupOnChainState();
|
|
672
750
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
673
751
|
mockBilling.getStatus.mockRejectedValue(new Error("API unavailable"));
|
|
674
|
-
const cmd = createCommand({ amount: "50" });
|
|
752
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
675
753
|
const promise = cmd.run();
|
|
676
754
|
await vi.advanceTimersByTimeAsync(2e5);
|
|
677
755
|
await promise;
|
|
@@ -680,5 +758,111 @@ describe("ecloud billing top-up", () => {
|
|
|
680
758
|
expect(fullOutput).toContain("Transaction confirmed");
|
|
681
759
|
expect(fullOutput).toContain("Credits haven't appeared yet");
|
|
682
760
|
});
|
|
761
|
+
it("credit card: charges selected card on file", async () => {
|
|
762
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 35 });
|
|
763
|
+
mockBilling.getPaymentMethods.mockResolvedValue({
|
|
764
|
+
paymentMethods: [
|
|
765
|
+
{
|
|
766
|
+
id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
|
|
767
|
+
stripePaymentMethodId: "pm_1ABC1234",
|
|
768
|
+
brand: "visa",
|
|
769
|
+
last4: "1234",
|
|
770
|
+
createdAt: "2026-04-20T15:00:00Z"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
id: "139752fd-4e6d-22f2-a97d-6712232dcg7e",
|
|
774
|
+
stripePaymentMethodId: "pm_2DEF5678",
|
|
775
|
+
brand: "mastercard",
|
|
776
|
+
last4: "5678",
|
|
777
|
+
createdAt: "2026-04-21T10:00:00Z"
|
|
778
|
+
}
|
|
779
|
+
]
|
|
780
|
+
});
|
|
781
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
782
|
+
purchaseId: "a1b2c3d4",
|
|
783
|
+
amountCents: "2500"
|
|
784
|
+
});
|
|
785
|
+
select3.mockResolvedValue("029641fc-3e5c-11f1-986c-5601121cbf6d");
|
|
786
|
+
const cmd = createCommand({ amount: "25", method: "card" });
|
|
787
|
+
const promise = cmd.run();
|
|
788
|
+
for (let i = 0; i < 10; i++) {
|
|
789
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
790
|
+
}
|
|
791
|
+
await promise;
|
|
792
|
+
const fullOutput = logOutput.join("\n");
|
|
793
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, "029641fc-3e5c-11f1-986c-5601121cbf6d");
|
|
794
|
+
expect(fullOutput).toContain("Payment submitted");
|
|
795
|
+
expect(fullOutput).toContain("Credits received");
|
|
796
|
+
});
|
|
797
|
+
it("credit card: opens checkout when user selects add new card", async () => {
|
|
798
|
+
const openMock = (await import("open")).default;
|
|
799
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
800
|
+
mockBilling.getPaymentMethods.mockResolvedValue({
|
|
801
|
+
paymentMethods: [
|
|
802
|
+
{
|
|
803
|
+
id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
|
|
804
|
+
stripePaymentMethodId: "pm_1ABC1234",
|
|
805
|
+
brand: "visa",
|
|
806
|
+
last4: "1234",
|
|
807
|
+
createdAt: "2026-04-20T15:00:00Z"
|
|
808
|
+
}
|
|
809
|
+
]
|
|
810
|
+
});
|
|
811
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
812
|
+
checkoutSessionId: "cs_test_abc123",
|
|
813
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
814
|
+
amountCents: "2500"
|
|
815
|
+
});
|
|
816
|
+
select3.mockResolvedValue("new");
|
|
817
|
+
const cmd = createCommand({ amount: "25", method: "card" });
|
|
818
|
+
const promise = cmd.run();
|
|
819
|
+
await vi.advanceTimersByTimeAsync(2e5);
|
|
820
|
+
await promise;
|
|
821
|
+
const fullOutput = logOutput.join("\n");
|
|
822
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, void 0);
|
|
823
|
+
expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
|
|
824
|
+
expect(fullOutput).toContain("https://checkout.stripe.com/test");
|
|
825
|
+
});
|
|
826
|
+
it("credit card: opens checkout when no card on file", async () => {
|
|
827
|
+
const openMock = (await import("open")).default;
|
|
828
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
829
|
+
mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
|
|
830
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
831
|
+
checkoutSessionId: "cs_test_abc123",
|
|
832
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
833
|
+
amountCents: "5000"
|
|
834
|
+
});
|
|
835
|
+
const cmd = createCommand({ amount: "50", method: "card" });
|
|
836
|
+
const promise = cmd.run();
|
|
837
|
+
await vi.advanceTimersByTimeAsync(2e5);
|
|
838
|
+
await promise;
|
|
839
|
+
const fullOutput = logOutput.join("\n");
|
|
840
|
+
expect(select3).not.toHaveBeenCalled();
|
|
841
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(5e3, void 0);
|
|
842
|
+
expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
|
|
843
|
+
expect(fullOutput).toContain("https://checkout.stripe.com/test");
|
|
844
|
+
});
|
|
845
|
+
it("credit card: rejects amount below $5 minimum", async () => {
|
|
846
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
847
|
+
const cmd = createCommand({ amount: "3", method: "card" });
|
|
848
|
+
await expect(cmd.run()).rejects.toThrow("Minimum purchase is $5");
|
|
849
|
+
});
|
|
850
|
+
it("credit card: --method and --amount flags skip prompts", async () => {
|
|
851
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
852
|
+
mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
|
|
853
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
854
|
+
checkoutSessionId: "cs_test_abc123",
|
|
855
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
856
|
+
amountCents: "5000"
|
|
857
|
+
});
|
|
858
|
+
const cmd = createCommand({ amount: "50", method: "card" });
|
|
859
|
+
const promise = cmd.run();
|
|
860
|
+
for (let i = 0; i < 10; i++) {
|
|
861
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
862
|
+
}
|
|
863
|
+
await promise;
|
|
864
|
+
expect(select3).not.toHaveBeenCalled();
|
|
865
|
+
expect(input3).not.toHaveBeenCalled();
|
|
866
|
+
});
|
|
683
867
|
});
|
|
684
868
|
//# sourceMappingURL=top-up.test.js.map
|