"Six laser sessions" is the common case in an aesthetics clinic: the patient pays once and books the sessions later. Credit is a ledger, not a counter. No table has a remaining/used_count column and a schema test enforces that — the balance is always SUM(delta) over append-only rows, so every number a patient sees has a full history behind it. Corrections are new rows, never edits. - purchase / consume / refund / adjustment / expiry, each with a reason, an author and the appointment it belongs to - consume happens in confirm(), never in quote(): if the preview consumed, a page refresh would cost the patient a session - cancelling adds a refund row; the consume row stays - FIFO across a patient's packages — the oldest is closest to expiring - an empty package is not an error, it just does not apply and the patient pays - adjust/expire need a doctor or clinic role, and adjust always needs a reason - app:package:expire writes the closing row so "where did my 3 sessions go?" always has an answer Consume takes a pessimistic lock on the one package row. That is the opposite of task 07's slot buckets, and docs/api/package.md carries the table explaining why, so nobody unifies them later. Idempotency checks for an existing consume row before inserting rather than catching the unique violation: in Doctrine that exception closes the EntityManager and burns the rest of the request. The unique key stays as the last line of defence. Admin: PackagesPage, a packages tab on the patient record, and a ledger page whose running-balance column shows where the final number came from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
56 lines
5.5 KiB
PHP
56 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
/**
|
|
* Auto-generated Migration: Please modify to your needs!
|
|
*/
|
|
final class Version20260731072023 extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return '';
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
// this up() migration is auto-generated, please modify it to your needs
|
|
$this->addSql('CREATE TABLE package_services (id INT AUTO_INCREMENT NOT NULL, package_id INT NOT NULL, service_item_id INT NOT NULL, INDEX IDX_97B37507F44CABFF (package_id), INDEX IDX_97B37507DDEB00C2 (service_item_id), UNIQUE INDEX uniq_pkg_service (package_id, service_item_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
|
$this->addSql('CREATE TABLE packages (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, name VARCHAR(200) NOT NULL, session_count SMALLINT NOT NULL, price_rials BIGINT NOT NULL, validity_days SMALLINT DEFAULT NULL, active TINYINT DEFAULT 1 NOT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, UNIQUE INDEX UNIQ_9BB5C0A7D17F50A6 (uuid), INDEX idx_packages_tenant (entity_type, entity_id, active), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
|
$this->addSql('CREATE TABLE patient_packages (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, session_count SMALLINT NOT NULL, price_paid_rials BIGINT NOT NULL, purchased_at INT NOT NULL, valid_to INT DEFAULT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, package_id INT NOT NULL, patient_record_id INT NOT NULL, payment_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_AC309802D17F50A6 (uuid), INDEX IDX_AC309802F44CABFF (package_id), INDEX IDX_AC309802EB76A733 (patient_record_id), INDEX IDX_AC3098024C3A3BB (payment_id), INDEX idx_pp_tenant (entity_type, entity_id, purchased_at), INDEX idx_pp_patient (patient_record_id, valid_to), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
|
$this->addSql('CREATE TABLE session_credit_ledger (id BIGINT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, kind VARCHAR(15) NOT NULL, delta SMALLINT NOT NULL, reason VARCHAR(255) DEFAULT NULL, created_at INT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, patient_package_id INT NOT NULL, appointment_id INT DEFAULT NULL, service_item_id INT DEFAULT NULL, created_by INT DEFAULT NULL, UNIQUE INDEX UNIQ_6B3C6BD6D17F50A6 (uuid), INDEX IDX_6B3C6BD6428C0D10 (patient_package_id), INDEX IDX_6B3C6BD6DDEB00C2 (service_item_id), INDEX IDX_6B3C6BD6DE12AB56 (created_by), INDEX idx_scl_package (patient_package_id, created_at), INDEX idx_scl_tenant (entity_type, entity_id, created_at), INDEX idx_scl_appt (appointment_id), UNIQUE INDEX uniq_scl_consume (appointment_id, kind), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
|
$this->addSql('ALTER TABLE package_services ADD CONSTRAINT FK_97B37507F44CABFF FOREIGN KEY (package_id) REFERENCES packages (id) ON DELETE CASCADE');
|
|
$this->addSql('ALTER TABLE package_services ADD CONSTRAINT FK_97B37507DDEB00C2 FOREIGN KEY (service_item_id) REFERENCES service_items (id) ON DELETE RESTRICT');
|
|
$this->addSql('ALTER TABLE patient_packages ADD CONSTRAINT FK_AC309802F44CABFF FOREIGN KEY (package_id) REFERENCES packages (id) ON DELETE RESTRICT');
|
|
$this->addSql('ALTER TABLE patient_packages ADD CONSTRAINT FK_AC309802EB76A733 FOREIGN KEY (patient_record_id) REFERENCES patient_records (id) ON DELETE RESTRICT');
|
|
$this->addSql('ALTER TABLE patient_packages ADD CONSTRAINT FK_AC3098024C3A3BB FOREIGN KEY (payment_id) REFERENCES payments (id) ON DELETE SET NULL');
|
|
$this->addSql('ALTER TABLE session_credit_ledger ADD CONSTRAINT FK_6B3C6BD6428C0D10 FOREIGN KEY (patient_package_id) REFERENCES patient_packages (id) ON DELETE RESTRICT');
|
|
$this->addSql('ALTER TABLE session_credit_ledger ADD CONSTRAINT FK_6B3C6BD6E5B533F9 FOREIGN KEY (appointment_id) REFERENCES appointments (id) ON DELETE SET NULL');
|
|
$this->addSql('ALTER TABLE session_credit_ledger ADD CONSTRAINT FK_6B3C6BD6DDEB00C2 FOREIGN KEY (service_item_id) REFERENCES service_items (id) ON DELETE SET NULL');
|
|
$this->addSql('ALTER TABLE session_credit_ledger ADD CONSTRAINT FK_6B3C6BD6DE12AB56 FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL');
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// this down() migration is auto-generated, please modify it to your needs
|
|
$this->addSql('ALTER TABLE package_services DROP FOREIGN KEY FK_97B37507F44CABFF');
|
|
$this->addSql('ALTER TABLE package_services DROP FOREIGN KEY FK_97B37507DDEB00C2');
|
|
$this->addSql('ALTER TABLE patient_packages DROP FOREIGN KEY FK_AC309802F44CABFF');
|
|
$this->addSql('ALTER TABLE patient_packages DROP FOREIGN KEY FK_AC309802EB76A733');
|
|
$this->addSql('ALTER TABLE patient_packages DROP FOREIGN KEY FK_AC3098024C3A3BB');
|
|
$this->addSql('ALTER TABLE session_credit_ledger DROP FOREIGN KEY FK_6B3C6BD6428C0D10');
|
|
$this->addSql('ALTER TABLE session_credit_ledger DROP FOREIGN KEY FK_6B3C6BD6E5B533F9');
|
|
$this->addSql('ALTER TABLE session_credit_ledger DROP FOREIGN KEY FK_6B3C6BD6DDEB00C2');
|
|
$this->addSql('ALTER TABLE session_credit_ledger DROP FOREIGN KEY FK_6B3C6BD6DE12AB56');
|
|
$this->addSql('DROP TABLE package_services');
|
|
$this->addSql('DROP TABLE packages');
|
|
$this->addSql('DROP TABLE patient_packages');
|
|
$this->addSql('DROP TABLE session_credit_ledger');
|
|
}
|
|
}
|