create(); BillingAddress::factory()->create(['user_id' => $user->id]); $paymentOption = PaymentOption::factory()->create([ 'price_cents' => 4900, 'interval' => 'monthly', ...$optionOverrides, ]); return UserPaymentOption::factory()->create([ 'user_id' => $user->id, 'payment_option_id' => $paymentOption->id, 'status' => UserPaymentOptionStatus::Active->value, 'stripe_subscription_id' => null, 'current_period_start' => today()->subMonth(), 'current_period_end' => today()->subDay(), 'legacy_conditions' => null, ...$overrides, ]); } test('a due manual agreement gets a MAN invoice and the period advances', function () { Carbon::setTestNow('2026-06-12 08:00:00'); $agreement = manualAgreement([ 'current_period_end' => '2026-06-11', ]); $invoice = app(ManualInvoiceService::class)->invoiceFor($agreement); expect($invoice)->not->toBeNull(); expect($invoice->number)->toBe('MAN-00001'); expect($invoice->status)->toBe(InvoiceStatus::Open); expect($invoice->amount_cents)->toBe(4900); expect($invoice->tax_cents)->toBe(931); expect($invoice->total_cents)->toBe(5831); expect($invoice->due_date->toDateString())->toBe('2026-06-26'); $fresh = $agreement->fresh(); expect($fresh->current_period_start->toDateString())->toBe('2026-06-11'); expect($fresh->current_period_end->toDateString())->toBe('2026-07-11'); Carbon::setTestNow(); }); test('legacy_conditions override amounts and interval', function () { Carbon::setTestNow('2026-06-12 08:00:00'); $agreement = manualAgreement([ 'current_period_end' => '2026-06-10', 'legacy_conditions' => [ 'amount_cents' => 10000, 'tax_cents' => 1900, 'total_cents' => 11900, 'interval' => 'yearly', ], ]); $invoice = app(ManualInvoiceService::class)->invoiceFor($agreement); expect($invoice->amount_cents)->toBe(10000); expect($invoice->tax_cents)->toBe(1900); expect($invoice->total_cents)->toBe(11900); expect($agreement->fresh()->current_period_end->toDateString())->toBe('2027-06-10'); Carbon::setTestNow(); }); test('the invoice freezes the billing address as a snapshot', function () { $agreement = manualAgreement(); $agreement->user->billingAddress->update(['name' => 'Alpha GmbH', 'city' => 'Berlin']); $invoice = app(ManualInvoiceService::class)->invoiceFor($agreement); $agreement->user->billingAddress->update(['name' => 'Beta GmbH', 'city' => 'Hamburg']); expect($invoice->fresh()->invoiceBillingAddress->name)->toBe('Alpha GmbH'); expect($invoice->fresh()->invoiceBillingAddress->city)->toBe('Berlin'); }); test('agreements without a billing address are skipped and keep their period', function () { $agreement = manualAgreement(); $agreement->user->billingAddress->delete(); $periodEnd = $agreement->current_period_end->toDateString(); $invoice = app(ManualInvoiceService::class)->invoiceFor($agreement->fresh()); expect($invoice)->toBeNull(); expect(Invoice::count())->toBe(0); expect($agreement->fresh()->current_period_end->toDateString())->toBe($periodEnd); }); test('stripe-managed agreements are never picked up by the manual circle', function () { manualAgreement(['stripe_subscription_id' => 'sub_123']); expect(app(ManualInvoiceService::class)->duePaymentOptions())->toHaveCount(0); }); test('agreements with a future period end are not due', function () { manualAgreement(['current_period_end' => today()->addWeek()]); expect(app(ManualInvoiceService::class)->duePaymentOptions())->toHaveCount(0); }); test('the command invoices all due agreements', function () { manualAgreement(); manualAgreement(); $this->artisan(GenerateManualInvoices::class)->assertSuccessful(); expect(Invoice::count())->toBe(2); expect(Invoice::pluck('number')->sort()->values()->all())->toBe(['MAN-00001', 'MAN-00002']); }); test('the command dry-run does not create invoices', function () { manualAgreement(); $this->artisan(GenerateManualInvoices::class, ['--dry-run' => true])->assertSuccessful(); expect(Invoice::count())->toBe(0); });