owner and admin section

This commit is contained in:
2026-09-09 08:26:26 +02:00
parent de46a91aa8
commit 85702c3000
30 changed files with 5611 additions and 674 deletions
@@ -0,0 +1,69 @@
<?php
use Phinx\Migration\AbstractMigration;
/**
* Crea le tabelle per gli abbonamenti dei saloni:
* - subscription_plans: i piani disponibili (gestiti dall'admin)
* - shop_subscriptions: l'abbonamento corrente di ogni salone
*
* Modello: l'abbonamento è il "motore" che accende/spegne shops.active.
* expires_at è la data chiave: superata, il cron mette status=expired e spegne il salone.
*/
class CreateSubscriptions extends AbstractMigration
{
public function change(): void
{
// ---- subscription_plans ----
$plans = $this->table('subscription_plans', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]);
$plans
->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
->addColumn('name', 'string', ['limit' => 80])
->addColumn('description', 'string', ['limit' => 255, 'null' => true])
->addColumn('price', 'decimal', ['precision' => 8, 'scale' => 2, 'default' => '0.00'])
->addColumn('billing_period', 'enum', ['values' => ['monthly', 'yearly'], 'default' => 'monthly'])
->addColumn('stripe_price_id', 'string', ['limit' => 120, 'null' => true])
->addColumn('is_active', 'boolean', ['default' => true])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
->create();
// ---- shop_subscriptions ----
$subs = $this->table('shop_subscriptions', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]);
$subs
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
->addColumn('shop_id', 'biginteger', ['signed' => false])
->addColumn('plan_id', 'integer', ['signed' => false, 'null' => true])
->addColumn('status', 'enum', [
'values' => ['active', 'expired', 'blocked', 'trialing', 'pending'],
'default' => 'pending',
])
->addColumn('started_at', 'date', ['null' => true])
->addColumn('expires_at', 'date', ['null' => true])
->addColumn('block_reason', 'string', ['limit' => 255, 'null' => true])
->addColumn('stripe_customer_id', 'string', ['limit' => 120, 'null' => true])
->addColumn('stripe_subscription_id', 'string', ['limit' => 120, 'null' => true])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
->addIndex(['shop_id'], ['unique' => true, 'name' => 'uniq_shop'])
->addIndex(['status'], ['name' => 'idx_status'])
->addIndex(['expires_at'], ['name' => 'idx_expires'])
->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_shop'])
->addForeignKey('plan_id', 'subscription_plans', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_plan'])
->create();
}
}
@@ -0,0 +1,43 @@
<?php
use Phinx\Migration\AbstractMigration;
/**
* Tabella billing_profiles: dati fiscali per la fatturazione degli abbonamenti.
* Legata sia all'utente (chi paga) sia al salone (per cosa), così regge
* anche il caso futuro di un owner con più saloni e P.IVA diverse.
*/
class CreateBillingProfiles extends AbstractMigration
{
public function change(): void
{
$t = $this->table('billing_profiles', [
'id' => false,
'primary_key' => ['id'],
'engine' => 'InnoDB',
'encoding' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
]);
$t
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
->addColumn('user_id', 'integer', ['signed' => false])
->addColumn('shop_id', 'biginteger', ['signed' => false, 'null' => true])
->addColumn('business_name', 'string', ['limit' => 191])
->addColumn('vat_number', 'string', ['limit' => 20])
->addColumn('tax_code', 'string', ['limit' => 20, 'null' => true])
->addColumn('address', 'string', ['limit' => 255])
->addColumn('zip_code', 'string', ['limit' => 10])
->addColumn('city', 'string', ['limit' => 100])
->addColumn('province', 'string', ['limit' => 50, 'null' => true])
->addColumn('pec', 'string', ['limit' => 191, 'null' => true])
->addColumn('sdi_code', 'string', ['limit' => 7, 'null' => true])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
->addIndex(['user_id'], ['name' => 'idx_bp_user'])
->addIndex(['shop_id'], ['name' => 'idx_bp_shop'])
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_user'])
->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_shop'])
->create();
}
}
@@ -0,0 +1,42 @@
<?php
use Phinx\Migration\AbstractMigration;
/**
* Aggiunge il campo "target" (pubblico servito) a shops, staff e services.
* Tipo SET MySQL: si possono selezionare più valori insieme (es. 'uomo,donna').
* Valori: uomo, donna, bambino, unisex.
*
* Scelta SET vs tabelle di collegamento: le categorie sono 4, fisse e senza
* attributi propri, quindi il SET è più semplice e filtrabile con FIND_IN_SET()
* senza JOIN aggiuntive. Nota logica (da applicare in ricerca): "unisex" copre
* sia uomo che donna.
*/
class AddTargetAudience extends AbstractMigration
{
public function change(): void
{
$values = "'uomo','donna','bambino','unisex'";
// shops: default unisex (un salone di norma serve tutti finché non specifica)
$this->execute("
ALTER TABLE `shops`
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
AFTER `description`
");
// staff: chi tratta cosa. Default unisex.
$this->execute("
ALTER TABLE `staff`
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
AFTER `role`
");
// services: a chi è rivolto il servizio. Default unisex.
$this->execute("
ALTER TABLE `services`
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
AFTER `category`
");
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
use Phinx\Seed\AbstractSeed;
/**
* Inserisce due piani base d'esempio.
* I prezzi/nome sono modificabili poi dal pannello admin o ri-eseguendo il seed.
* stripe_price_id resta NULL: lo popoleremo quando colleghiamo Stripe.
*/
class SubscriptionPlansSeeder extends AbstractSeed
{
public function run(): void
{
// Evita duplicati se il seed viene rilanciato: inserisce solo se la tabella è vuota
$count = (int)$this->fetchRow("SELECT COUNT(*) AS c FROM subscription_plans")['c'];
if ($count > 0) {
return;
}
$data = [
[
'name' => 'Base',
'description' => 'Prenotazioni online, agenda e gestione clienti.',
'price' => 19.90,
'billing_period' => 'monthly',
'is_active' => 1,
],
[
'name' => 'Pro',
'description' => 'Tutto del piano Base + più staff, statistiche e priorità in ricerca.',
'price' => 39.90,
'billing_period' => 'monthly',
'is_active' => 1,
],
];
$this->table('subscription_plans')->insert($data)->saveData();
}
}