@layr-labs/ecloud-cli 0.5.0-dev.3 → 1.0.0-dev.1
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 +257 -89
- package/dist/commands/billing/__tests__/top-up.test.js.map +1 -1
- package/dist/commands/billing/status.js +4 -23
- package/dist/commands/billing/status.js.map +1 -1
- package/dist/commands/billing/top-up.js +145 -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, confirm } 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,156 @@ ${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 useExistingCard = false;
|
|
521
|
+
let paymentMethodId;
|
|
522
|
+
if (paymentMethods.length > 0) {
|
|
523
|
+
const card = paymentMethods[0];
|
|
524
|
+
const lastFour = card.stripePaymentMethodId.slice(-4);
|
|
525
|
+
useExistingCard = await confirm({
|
|
526
|
+
message: `Use card on file (ending in ${lastFour})?`,
|
|
527
|
+
default: true
|
|
528
|
+
});
|
|
529
|
+
if (useExistingCard) {
|
|
530
|
+
paymentMethodId = card.id;
|
|
468
531
|
}
|
|
532
|
+
}
|
|
533
|
+
this.log(`
|
|
534
|
+
Purchasing ${chalk2.bold(`$${dollars}`)} in credits...`);
|
|
535
|
+
const result = await billing.purchaseCredits(amountCents, paymentMethodId);
|
|
536
|
+
if (result.checkoutUrl) {
|
|
469
537
|
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
|
-
this.log(` ${chalk2.gray(`$${appliedFromTopUp.toFixed(2)} applied to current bill`)}`);
|
|
502
|
-
}
|
|
503
|
-
this.log();
|
|
504
|
-
return;
|
|
538
|
+
${chalk2.cyan(result.checkoutUrl)}`);
|
|
539
|
+
this.log(chalk2.gray(" Opening checkout in browser..."));
|
|
540
|
+
await open(result.checkoutUrl);
|
|
541
|
+
} else {
|
|
542
|
+
this.log(` ${chalk2.green("\u2713")} Payment submitted`);
|
|
543
|
+
}
|
|
544
|
+
await this.pollForCredits(billing, flags, baselineTotal, dollars);
|
|
545
|
+
}
|
|
546
|
+
async pollForCredits(billing, flags, baselineTotal, amountPurchased) {
|
|
547
|
+
this.log(chalk2.gray("\n Waiting for credits to appear..."));
|
|
548
|
+
const startTime = Date.now();
|
|
549
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
550
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
551
|
+
try {
|
|
552
|
+
const status = await billing.getStatus({
|
|
553
|
+
productId: flags.product
|
|
554
|
+
});
|
|
555
|
+
const remaining = status.remainingCredits ?? 0;
|
|
556
|
+
const applied = status.creditsApplied ?? 0;
|
|
557
|
+
const currentTotal = remaining + applied;
|
|
558
|
+
this.debug(
|
|
559
|
+
`Poll: remaining=${remaining}, applied=${applied}, total=${currentTotal}, baseline=${baselineTotal}`
|
|
560
|
+
);
|
|
561
|
+
if (baselineTotal === void 0 || currentTotal > baselineTotal) {
|
|
562
|
+
const creditsAdded = baselineTotal !== void 0 ? currentTotal - baselineTotal : void 0;
|
|
563
|
+
this.log(
|
|
564
|
+
`
|
|
565
|
+
${chalk2.green("\u2713")} Credits received: ${chalk2.cyan(`$${(creditsAdded ?? amountPurchased).toFixed(2)}`)}`
|
|
566
|
+
);
|
|
567
|
+
if (remaining > 0) {
|
|
568
|
+
this.log(` Remaining balance: ${chalk2.cyan(`$${remaining.toFixed(2)}`)}`);
|
|
505
569
|
}
|
|
506
|
-
|
|
507
|
-
|
|
570
|
+
this.log();
|
|
571
|
+
return;
|
|
508
572
|
}
|
|
573
|
+
} catch {
|
|
574
|
+
this.debug("Error polling for credit balance");
|
|
509
575
|
}
|
|
510
|
-
|
|
511
|
-
|
|
576
|
+
}
|
|
577
|
+
this.log(
|
|
578
|
+
`
|
|
512
579
|
${chalk2.yellow("\u26A0")} Credits haven't appeared yet. This can take a few minutes.`
|
|
513
|
-
|
|
514
|
-
|
|
580
|
+
);
|
|
581
|
+
this.log(` ${chalk2.gray("Check your balance:")} ecloud billing status
|
|
515
582
|
`);
|
|
516
|
-
});
|
|
517
583
|
}
|
|
518
584
|
};
|
|
519
585
|
|
|
520
586
|
// src/commands/billing/__tests__/top-up.test.ts
|
|
521
|
-
import { input as input3 } from "@inquirer/prompts";
|
|
587
|
+
import { input as input3, select as select3, confirm as confirm2 } from "@inquirer/prompts";
|
|
522
588
|
vi.mock("../../../client", () => ({
|
|
523
589
|
createBillingClient: vi.fn()
|
|
524
590
|
}));
|
|
@@ -526,7 +592,12 @@ vi.mock("../../../telemetry", () => ({
|
|
|
526
592
|
withTelemetry: vi.fn((_cmd, fn) => fn())
|
|
527
593
|
}));
|
|
528
594
|
vi.mock("@inquirer/prompts", () => ({
|
|
529
|
-
input: vi.fn()
|
|
595
|
+
input: vi.fn(),
|
|
596
|
+
select: vi.fn(),
|
|
597
|
+
confirm: vi.fn()
|
|
598
|
+
}));
|
|
599
|
+
vi.mock("open", () => ({
|
|
600
|
+
default: vi.fn()
|
|
530
601
|
}));
|
|
531
602
|
var WALLET_ADDRESS = "0x1234567890abcdef1234567890abcdef12345678";
|
|
532
603
|
var TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
|
|
@@ -541,7 +612,9 @@ describe("ecloud billing top-up", () => {
|
|
|
541
612
|
address: WALLET_ADDRESS,
|
|
542
613
|
getStatus: vi.fn(),
|
|
543
614
|
getTopUpInfo: vi.fn(),
|
|
544
|
-
topUp: vi.fn()
|
|
615
|
+
topUp: vi.fn(),
|
|
616
|
+
getPaymentMethods: vi.fn(),
|
|
617
|
+
purchaseCredits: vi.fn()
|
|
545
618
|
};
|
|
546
619
|
createBillingClient.mockResolvedValue(mockBilling);
|
|
547
620
|
input3.mockResolvedValue("50");
|
|
@@ -586,7 +659,7 @@ describe("ecloud billing top-up", () => {
|
|
|
586
659
|
setupOnChainState();
|
|
587
660
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
588
661
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
589
|
-
const cmd = createCommand({ amount: "50" });
|
|
662
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
590
663
|
const promise = cmd.run();
|
|
591
664
|
for (let i = 0; i < 10; i++) {
|
|
592
665
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -608,7 +681,7 @@ describe("ecloud billing top-up", () => {
|
|
|
608
681
|
it("zero USDC balance: exits with fund wallet message", async () => {
|
|
609
682
|
setupOnChainState({ usdcBalance: BigInt(0) });
|
|
610
683
|
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
|
|
611
|
-
const cmd = createCommand({ amount: "50" });
|
|
684
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
612
685
|
await cmd.run();
|
|
613
686
|
const fullOutput = logOutput.join("\n");
|
|
614
687
|
expect(fullOutput).toContain("No USDC in wallet");
|
|
@@ -619,7 +692,7 @@ describe("ecloud billing top-up", () => {
|
|
|
619
692
|
it("below minimum purchase: shows error", async () => {
|
|
620
693
|
setupOnChainState({ minimumPurchase: BigInt(1e7) });
|
|
621
694
|
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "inactive" });
|
|
622
|
-
const cmd = createCommand({ amount: "5" });
|
|
695
|
+
const cmd = createCommand({ amount: "5", method: "usdc" });
|
|
623
696
|
await expect(cmd.run()).rejects.toThrow("Minimum purchase is 10 USDC");
|
|
624
697
|
});
|
|
625
698
|
it("--account flag: passes different address to topUp", async () => {
|
|
@@ -627,7 +700,7 @@ describe("ecloud billing top-up", () => {
|
|
|
627
700
|
setupOnChainState();
|
|
628
701
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
629
702
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
630
|
-
const cmd = createCommand({ amount: "50", account: targetAccount });
|
|
703
|
+
const cmd = createCommand({ amount: "50", method: "usdc", account: targetAccount });
|
|
631
704
|
const promise = cmd.run();
|
|
632
705
|
for (let i = 0; i < 10; i++) {
|
|
633
706
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -647,7 +720,7 @@ describe("ecloud billing top-up", () => {
|
|
|
647
720
|
subscriptionStatus: "active",
|
|
648
721
|
remainingCredits: 10
|
|
649
722
|
});
|
|
650
|
-
const cmd = createCommand({ amount: "50" });
|
|
723
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
651
724
|
const promise = cmd.run();
|
|
652
725
|
await vi.advanceTimersByTimeAsync(2e5);
|
|
653
726
|
await promise;
|
|
@@ -659,7 +732,7 @@ describe("ecloud billing top-up", () => {
|
|
|
659
732
|
setupOnChainState();
|
|
660
733
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
661
734
|
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "inactive" }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 100 });
|
|
662
|
-
const cmd = createCommand({ amount: "100" });
|
|
735
|
+
const cmd = createCommand({ amount: "100", method: "usdc" });
|
|
663
736
|
const promise = cmd.run();
|
|
664
737
|
for (let i = 0; i < 10; i++) {
|
|
665
738
|
await vi.advanceTimersByTimeAsync(5e3);
|
|
@@ -671,7 +744,7 @@ describe("ecloud billing top-up", () => {
|
|
|
671
744
|
setupOnChainState();
|
|
672
745
|
mockBilling.topUp.mockResolvedValue({ txHash: TX_HASH, walletAddress: WALLET_ADDRESS });
|
|
673
746
|
mockBilling.getStatus.mockRejectedValue(new Error("API unavailable"));
|
|
674
|
-
const cmd = createCommand({ amount: "50" });
|
|
747
|
+
const cmd = createCommand({ amount: "50", method: "usdc" });
|
|
675
748
|
const promise = cmd.run();
|
|
676
749
|
await vi.advanceTimersByTimeAsync(2e5);
|
|
677
750
|
await promise;
|
|
@@ -680,5 +753,100 @@ describe("ecloud billing top-up", () => {
|
|
|
680
753
|
expect(fullOutput).toContain("Transaction confirmed");
|
|
681
754
|
expect(fullOutput).toContain("Credits haven't appeared yet");
|
|
682
755
|
});
|
|
756
|
+
it("credit card: charges existing card on file", async () => {
|
|
757
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 35 });
|
|
758
|
+
mockBilling.getPaymentMethods.mockResolvedValue({
|
|
759
|
+
paymentMethods: [
|
|
760
|
+
{
|
|
761
|
+
id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
|
|
762
|
+
stripePaymentMethodId: "pm_1ABC1234",
|
|
763
|
+
createdAt: "2026-04-20T15:00:00Z"
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
768
|
+
purchaseId: "a1b2c3d4",
|
|
769
|
+
amountCents: "2500"
|
|
770
|
+
});
|
|
771
|
+
confirm2.mockResolvedValue(true);
|
|
772
|
+
const cmd = createCommand({ amount: "25", method: "card" });
|
|
773
|
+
const promise = cmd.run();
|
|
774
|
+
for (let i = 0; i < 10; i++) {
|
|
775
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
776
|
+
}
|
|
777
|
+
await promise;
|
|
778
|
+
const fullOutput = logOutput.join("\n");
|
|
779
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, "029641fc-3e5c-11f1-986c-5601121cbf6d");
|
|
780
|
+
expect(fullOutput).toContain("Payment submitted");
|
|
781
|
+
expect(fullOutput).toContain("Credits received");
|
|
782
|
+
});
|
|
783
|
+
it("credit card: opens checkout when user declines existing card", async () => {
|
|
784
|
+
const openMock = (await import("open")).default;
|
|
785
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
786
|
+
mockBilling.getPaymentMethods.mockResolvedValue({
|
|
787
|
+
paymentMethods: [
|
|
788
|
+
{
|
|
789
|
+
id: "029641fc-3e5c-11f1-986c-5601121cbf6d",
|
|
790
|
+
stripePaymentMethodId: "pm_1ABC1234",
|
|
791
|
+
createdAt: "2026-04-20T15:00:00Z"
|
|
792
|
+
}
|
|
793
|
+
]
|
|
794
|
+
});
|
|
795
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
796
|
+
checkoutSessionId: "cs_test_abc123",
|
|
797
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
798
|
+
amountCents: "2500"
|
|
799
|
+
});
|
|
800
|
+
confirm2.mockResolvedValue(false);
|
|
801
|
+
const cmd = createCommand({ amount: "25", method: "card" });
|
|
802
|
+
const promise = cmd.run();
|
|
803
|
+
await vi.advanceTimersByTimeAsync(2e5);
|
|
804
|
+
await promise;
|
|
805
|
+
const fullOutput = logOutput.join("\n");
|
|
806
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(2500, void 0);
|
|
807
|
+
expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
|
|
808
|
+
expect(fullOutput).toContain("https://checkout.stripe.com/test");
|
|
809
|
+
});
|
|
810
|
+
it("credit card: opens checkout when no card on file", async () => {
|
|
811
|
+
const openMock = (await import("open")).default;
|
|
812
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
813
|
+
mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
|
|
814
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
815
|
+
checkoutSessionId: "cs_test_abc123",
|
|
816
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
817
|
+
amountCents: "5000"
|
|
818
|
+
});
|
|
819
|
+
const cmd = createCommand({ amount: "50", method: "card" });
|
|
820
|
+
const promise = cmd.run();
|
|
821
|
+
await vi.advanceTimersByTimeAsync(2e5);
|
|
822
|
+
await promise;
|
|
823
|
+
const fullOutput = logOutput.join("\n");
|
|
824
|
+
expect(confirm2).not.toHaveBeenCalled();
|
|
825
|
+
expect(mockBilling.purchaseCredits).toHaveBeenCalledWith(5e3, void 0);
|
|
826
|
+
expect(openMock).toHaveBeenCalledWith("https://checkout.stripe.com/test");
|
|
827
|
+
expect(fullOutput).toContain("https://checkout.stripe.com/test");
|
|
828
|
+
});
|
|
829
|
+
it("credit card: rejects amount below $5 minimum", async () => {
|
|
830
|
+
mockBilling.getStatus.mockResolvedValue({ subscriptionStatus: "active", remainingCredits: 10 });
|
|
831
|
+
const cmd = createCommand({ amount: "3", method: "card" });
|
|
832
|
+
await expect(cmd.run()).rejects.toThrow("Minimum purchase is $5");
|
|
833
|
+
});
|
|
834
|
+
it("credit card: --method and --amount flags skip prompts", async () => {
|
|
835
|
+
mockBilling.getStatus.mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 10 }).mockResolvedValueOnce({ subscriptionStatus: "active", remainingCredits: 60 });
|
|
836
|
+
mockBilling.getPaymentMethods.mockResolvedValue({ paymentMethods: [] });
|
|
837
|
+
mockBilling.purchaseCredits.mockResolvedValue({
|
|
838
|
+
checkoutSessionId: "cs_test_abc123",
|
|
839
|
+
checkoutUrl: "https://checkout.stripe.com/test",
|
|
840
|
+
amountCents: "5000"
|
|
841
|
+
});
|
|
842
|
+
const cmd = createCommand({ amount: "50", method: "card" });
|
|
843
|
+
const promise = cmd.run();
|
|
844
|
+
for (let i = 0; i < 10; i++) {
|
|
845
|
+
await vi.advanceTimersByTimeAsync(5e3);
|
|
846
|
+
}
|
|
847
|
+
await promise;
|
|
848
|
+
expect(select3).not.toHaveBeenCalled();
|
|
849
|
+
expect(input3).not.toHaveBeenCalled();
|
|
850
|
+
});
|
|
683
851
|
});
|
|
684
852
|
//# sourceMappingURL=top-up.test.js.map
|