124 lines
5.2 KiB
PHP
124 lines
5.2 KiB
PHP
<?php
|
|
|
|
use App\Models\PlanPrice;
|
|
use App\Models\StripeAddonPrice;
|
|
use App\Models\StripePlanPrice;
|
|
use App\Services\Stripe\FakeStripeClient;
|
|
use App\Services\Stripe\StripeClient;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
|
|
/**
|
|
* The orphans an operator can finally act on.
|
|
*
|
|
* The command lists EVERY active recurring Price at one of our Products that no
|
|
* row knows — adoptable or not; the only question it asks is whether a register
|
|
* row holds the id. That is wider than the log line AdoptStripePrice writes, and
|
|
* it is why stripe:sync-catalogue has to run first: see the command's own
|
|
* docblock, which had to be corrected away from the narrower claim.
|
|
*
|
|
* The fixtures below are all at the narrow end anyway — planted with no metadata
|
|
* at all, so nothing could have adopted them, which is what a person clicking
|
|
* through Stripe's dashboard leaves behind. That keeps each test about the
|
|
* sweep's own question (does a row know this id?) rather than about what the
|
|
* recognition step would have done with it.
|
|
*/
|
|
beforeEach(function () {
|
|
$this->stripe = new FakeStripeClient;
|
|
app()->instance(StripeClient::class, $this->stripe);
|
|
app(Kernel::class)->call('stripe:sync-catalogue');
|
|
|
|
$this->product = (string) StripeAddonPrice::query()
|
|
->where('addon_key', 'priority_support')
|
|
->value('stripe_product_id');
|
|
|
|
$this->known = (string) StripeAddonPrice::query()
|
|
->where('addon_key', 'priority_support')
|
|
->where('interval', 'month')
|
|
->where('reverse_charge', false)
|
|
->value('stripe_price_id');
|
|
|
|
$this->stripe->plantPrice('price_orphan', $this->product, 9900, 'EUR', 'month', []);
|
|
});
|
|
|
|
it('reports the orphan and leaves it alone', function () {
|
|
$this->artisan('stripe:sweep-orphan-prices')
|
|
->expectsOutputToContain('price_orphan')
|
|
->assertSuccessful();
|
|
|
|
expect($this->stripe->archived)->toBe([]);
|
|
});
|
|
|
|
it('never reports a price a row knows', function () {
|
|
$this->artisan('stripe:sweep-orphan-prices')
|
|
->doesntExpectOutputToContain($this->known)
|
|
->assertSuccessful();
|
|
});
|
|
|
|
it('sees the plan side too: a family product orphan is reported, a known plan price never', function () {
|
|
// Every other test in this file plants its orphan under the MODULE Product,
|
|
// so both halves of the plan side went untested: the PlanFamily half of
|
|
// products() and the StripePlanPrice half of the $known set. Blind to
|
|
// either, the command misses the LARGER half — a family Product collects a
|
|
// Price per version, term, treatment and rate change, a module Product four.
|
|
//
|
|
// Both assertions sit in one test on purpose: each fails for one of the two
|
|
// lines. Drop the PlanFamily pluck from products() and the family Product is
|
|
// never visited, so the orphan below goes unreported. Drop the
|
|
// StripePlanPrice pluck from $known and the row's own Price becomes an
|
|
// orphan the command names.
|
|
$row = PlanPrice::query()->firstOrFail();
|
|
$familyProduct = (string) $row->version->family->stripe_product_id;
|
|
|
|
$knownPlanPrice = (string) StripePlanPrice::query()
|
|
->where('plan_price_id', $row->id)
|
|
->where('reverse_charge', false)
|
|
->value('stripe_price_id');
|
|
|
|
// At the family's Product and at an amount no row of ours holds — what the
|
|
// sync leaves behind when it dies between Stripe's create and our insert,
|
|
// seen from the sweep's side.
|
|
$this->stripe->plantPrice('price_family_orphan', $familyProduct,
|
|
12300, (string) $row->currency, 'month', []);
|
|
|
|
$this->artisan('stripe:sweep-orphan-prices')
|
|
->expectsOutputToContain('price_family_orphan')
|
|
->doesntExpectOutputToContain($knownPlanPrice)
|
|
->assertSuccessful();
|
|
|
|
expect($familyProduct)->not->toBe('')
|
|
->and($knownPlanPrice)->not->toBe('')
|
|
// Nothing was touched: this is the reporting path, not --archive.
|
|
->and($this->stripe->archived)->toBe([]);
|
|
});
|
|
|
|
it('stops selling the orphan when asked to', function () {
|
|
$this->artisan('stripe:sweep-orphan-prices', ['--archive' => true])
|
|
->assertSuccessful();
|
|
|
|
// Archiving is harmless for the same reason it always is here: Stripe goes
|
|
// on billing every subscription already on an archived Price, it merely
|
|
// stops being sold.
|
|
expect($this->stripe->archived)->toBe(['price_orphan']);
|
|
});
|
|
|
|
it('touches nothing on a dry run, even when asked to archive', function () {
|
|
$this->artisan('stripe:sweep-orphan-prices', ['--archive' => true, '--dry-run' => true])
|
|
->expectsOutputToContain('price_orphan')
|
|
->assertSuccessful();
|
|
|
|
expect($this->stripe->archived)->toBe([]);
|
|
});
|
|
|
|
it('fails cleanly on a dry run when Stripe is not configured, rather than throwing', function () {
|
|
// Unlike stripe:sync-catalogue, this command has no local-only path: the
|
|
// report itself is a live activePricesFor() call, dry-run or not. A guard
|
|
// that only fired outside --dry-run would let this fall straight into
|
|
// HttpStripeClient's own "not configured" exception instead of the clean
|
|
// error below.
|
|
$this->stripe->configured = false;
|
|
|
|
$this->artisan('stripe:sweep-orphan-prices', ['--dry-run' => true])
|
|
->expectsOutputToContain('Stripe is not configured (STRIPE_SECRET is empty). Nothing was read.')
|
|
->assertFailed();
|
|
});
|