Add payments foundation (e-transfer/comp, Stripe config, receipts)
CI / Tests (PHP 8.1) (pull_request) Successful in 45s
CI / Tests (PHP 8.3) (pull_request) Successful in 50s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 1m2s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m0s
CI / PHPStan (pull_request) Successful in 1m4s
CI / Build Plugin Zip (pull_request) Has been skipped

Implements the payments foundation for #7. Without Stripe credentials
everything works on e-transfer (pending payment confirmed by a studio
admin); when Stripe keys are configured the default flips to credit card.
Per-student override (card/etransfer/comp) is set on the student detail.

- Schema: us_payments (amount DECIMAL dollars, method, status, receipt,
  stripe intent id).
- src/Payment/: Payment VO, PaymentRepository, StudioSettings (Stripe
  options + isStripeConfigured + settings page), BillingMethodResolver
  (per-student override; default card if configured else etransfer),
  ReceiptMailer, PaymentService (create at registration, link payment_id,
  comp->paid+confirm, markPaid->confirm+receipt), PaymentController
  (e-transfer confirmation queue), PaymentEndpoint (PATCH /payments/{id}).
- Booking + enrolment create the payment from the offering price; comp
  auto-confirms the lesson; setPaymentId on both repositories.
- Admin: Studio Settings + Payments menus (manage_billing); per-student
  billing method on the student detail page.
- Docs: payments.md + README updated.

Deferred to a follow-up: the live Stripe card charge (PaymentIntent +
Stripe.js Elements + webhook + stripe/stripe-php). Until then a card
payment is created pending and confirmed like an e-transfer.

Tests: tests/Unit/Payment/ (VO, repository, resolver, service, mailer).
composer test (147), cs, and PHPStan level 6 all pass.

Refs #7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 10:24:01 -03:00
parent 071ef7fc2a
commit 6c4097b385
27 changed files with 1201 additions and 12 deletions
+92
View File
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Payment;
class Payment {
public const METHOD_CARD = 'card';
public const METHOD_ETRANSFER = 'etransfer';
public const METHOD_COMP = 'comp';
/**
* All valid payment methods.
*
* @var list<string>
*/
public const VALID_METHODS = [ self::METHOD_CARD, self::METHOD_ETRANSFER, self::METHOD_COMP ];
public const STATUS_PENDING = 'pending';
public const STATUS_PAID = 'paid';
public const STATUS_FAILED = 'failed';
public const STATUS_REFUNDED = 'refunded';
/**
* All valid payment statuses.
*
* @var list<string>
*/
public const VALID_STATUSES = [ self::STATUS_PENDING, self::STATUS_PAID, self::STATUS_FAILED, self::STATUS_REFUNDED ];
public const REG_LESSON = 'lesson';
public const REG_ENROLLMENT = 'enrollment';
public function __construct(
public readonly int $studentId,
public readonly int $instructorId,
public readonly string $registrationType,
public readonly int $registrationId,
public readonly float $amount,
public readonly string $currency = 'CAD',
public readonly string $method = self::METHOD_ETRANSFER,
public readonly string $status = self::STATUS_PENDING,
public readonly ?string $stripePaymentIntentId = null,
public readonly ?string $receiptNumber = null,
public readonly ?string $receiptSentAt = null,
public readonly ?string $paidAt = null,
public readonly ?int $id = null,
) {}
public static function fromRow( object $row ): self {
return new self(
studentId: (int) $row->student_id,
instructorId: (int) $row->instructor_id,
registrationType: $row->registration_type,
registrationId: (int) $row->registration_id,
amount: (float) $row->amount,
currency: $row->currency,
method: $row->method,
status: $row->status,
stripePaymentIntentId: $row->stripe_payment_intent_id,
receiptNumber: $row->receipt_number,
receiptSentAt: $row->receipt_sent_at,
paidAt: $row->paid_at,
id: (int) $row->id,
);
}
public function isPaid(): bool {
return self::STATUS_PAID === $this->status;
}
/**
* Returns a plain array representation of the payment.
*
* @return array<string, mixed>
*/
public function toArray(): array {
return [
'id' => $this->id,
'student_id' => $this->studentId,
'instructor_id' => $this->instructorId,
'registration_type' => $this->registrationType,
'registration_id' => $this->registrationId,
'amount' => $this->amount,
'currency' => $this->currency,
'method' => $this->method,
'status' => $this->status,
'receipt_number' => $this->receiptNumber,
'paid_at' => $this->paidAt,
];
}
}