2026-04-06 10:55:56 +02:00
|
|
|
import 'package:equatable/equatable.dart';
|
|
|
|
|
|
2026-04-22 11:06:02 +02:00
|
|
|
// ===================================================================
|
|
|
|
|
// ENUMS (Paranoia Mode per la lettura dal DB)
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
enum SubscriptionTier {
|
|
|
|
|
free,
|
|
|
|
|
pro,
|
|
|
|
|
premium,
|
|
|
|
|
platinum;
|
|
|
|
|
|
|
|
|
|
static SubscriptionTier fromString(String? value) {
|
|
|
|
|
return SubscriptionTier.values.firstWhere(
|
|
|
|
|
(e) => e.name == value,
|
|
|
|
|
orElse: () => SubscriptionTier.free, // Fallback di sicurezza
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum SubscriptionStatus {
|
|
|
|
|
trialing,
|
|
|
|
|
active,
|
|
|
|
|
pastDue,
|
|
|
|
|
canceled;
|
|
|
|
|
|
|
|
|
|
static SubscriptionStatus fromString(String? value) {
|
|
|
|
|
if (value == null) return SubscriptionStatus.canceled;
|
|
|
|
|
// Normalizziamo 'past_due' dal DB in 'pastdue' per il match con l'Enum
|
|
|
|
|
final normalizedValue = value.replaceAll('_', '').toLowerCase();
|
|
|
|
|
return SubscriptionStatus.values.firstWhere(
|
|
|
|
|
(e) => e.name.toLowerCase() == normalizedValue,
|
|
|
|
|
orElse: () => SubscriptionStatus.canceled,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// IL MODELLO ESATTO
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
2026-04-06 10:55:56 +02:00
|
|
|
class CompanyModel extends Equatable {
|
2026-04-22 11:06:02 +02:00
|
|
|
final String? id;
|
2026-04-09 11:30:57 +02:00
|
|
|
final DateTime? createdAt;
|
2026-04-22 11:06:02 +02:00
|
|
|
final String userId; // Nel DB è user_id (chiave esterna su auth.users)
|
|
|
|
|
|
|
|
|
|
// Dati Anagrafici e Fatturazione
|
2026-04-06 10:55:56 +02:00
|
|
|
final String ragioneSociale;
|
|
|
|
|
final String indirizzo;
|
|
|
|
|
final String cap;
|
|
|
|
|
final String citta;
|
|
|
|
|
final String provincia;
|
|
|
|
|
final String partitaIva;
|
|
|
|
|
final String codiceFiscale;
|
|
|
|
|
final String codiceUnivoco;
|
2026-04-22 11:06:02 +02:00
|
|
|
final String companyLogo;
|
|
|
|
|
|
|
|
|
|
// Stato Pagamenti (Ibride: manuale + Stripe)
|
2026-04-06 10:55:56 +02:00
|
|
|
final bool isPaid;
|
|
|
|
|
final DateTime? paymentExpiration;
|
2026-04-22 11:06:02 +02:00
|
|
|
|
|
|
|
|
// Campi SaaS Stripe/Automazioni
|
|
|
|
|
final SubscriptionTier subscriptionTier;
|
|
|
|
|
final SubscriptionStatus subscriptionStatus;
|
|
|
|
|
final DateTime? trialEndsAt;
|
|
|
|
|
final String? stripeCustomerId;
|
|
|
|
|
final String? stripeSubscriptionId;
|
2026-04-06 10:55:56 +02:00
|
|
|
|
|
|
|
|
const CompanyModel({
|
2026-04-22 11:06:02 +02:00
|
|
|
this.id,
|
2026-04-09 11:30:57 +02:00
|
|
|
this.createdAt,
|
2026-04-06 10:55:56 +02:00
|
|
|
required this.userId,
|
|
|
|
|
required this.ragioneSociale,
|
|
|
|
|
required this.indirizzo,
|
|
|
|
|
required this.cap,
|
|
|
|
|
required this.citta,
|
|
|
|
|
required this.provincia,
|
|
|
|
|
required this.partitaIva,
|
|
|
|
|
required this.codiceFiscale,
|
|
|
|
|
required this.codiceUnivoco,
|
2026-04-22 11:06:02 +02:00
|
|
|
this.companyLogo = '',
|
2026-04-09 11:30:57 +02:00
|
|
|
this.isPaid = false,
|
2026-04-06 10:55:56 +02:00
|
|
|
this.paymentExpiration,
|
2026-04-22 11:06:02 +02:00
|
|
|
this.subscriptionTier = SubscriptionTier.free,
|
|
|
|
|
this.subscriptionStatus = SubscriptionStatus.trialing,
|
|
|
|
|
this.trialEndsAt,
|
|
|
|
|
this.stripeCustomerId,
|
|
|
|
|
this.stripeSubscriptionId,
|
2026-04-06 10:55:56 +02:00
|
|
|
});
|
|
|
|
|
|
2026-04-22 11:06:02 +02:00
|
|
|
CompanyModel copyWith({
|
|
|
|
|
String? id,
|
|
|
|
|
DateTime? createdAt,
|
|
|
|
|
String? userId,
|
|
|
|
|
String? ragioneSociale,
|
|
|
|
|
String? indirizzo,
|
|
|
|
|
String? cap,
|
|
|
|
|
String? citta,
|
|
|
|
|
String? provincia,
|
|
|
|
|
String? partitaIva,
|
|
|
|
|
String? codiceFiscale,
|
|
|
|
|
String? codiceUnivoco,
|
|
|
|
|
String? companyLogo,
|
|
|
|
|
bool? isPaid,
|
|
|
|
|
DateTime? paymentExpiration,
|
|
|
|
|
SubscriptionTier? subscriptionTier,
|
|
|
|
|
SubscriptionStatus? subscriptionStatus,
|
|
|
|
|
DateTime? trialEndsAt,
|
|
|
|
|
String? stripeCustomerId,
|
|
|
|
|
String? stripeSubscriptionId,
|
|
|
|
|
}) {
|
|
|
|
|
return CompanyModel(
|
|
|
|
|
id: id ?? this.id,
|
|
|
|
|
createdAt: createdAt ?? this.createdAt,
|
|
|
|
|
userId: userId ?? this.userId,
|
|
|
|
|
ragioneSociale: ragioneSociale ?? this.ragioneSociale,
|
|
|
|
|
indirizzo: indirizzo ?? this.indirizzo,
|
|
|
|
|
cap: cap ?? this.cap,
|
|
|
|
|
citta: citta ?? this.citta,
|
|
|
|
|
provincia: provincia ?? this.provincia,
|
|
|
|
|
partitaIva: partitaIva ?? this.partitaIva,
|
|
|
|
|
codiceFiscale: codiceFiscale ?? this.codiceFiscale,
|
|
|
|
|
codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco,
|
|
|
|
|
companyLogo: companyLogo ?? this.companyLogo,
|
|
|
|
|
isPaid: isPaid ?? this.isPaid,
|
|
|
|
|
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
|
|
|
|
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
|
|
|
|
subscriptionStatus: subscriptionStatus ?? this.subscriptionStatus,
|
|
|
|
|
trialEndsAt: trialEndsAt ?? this.trialEndsAt,
|
|
|
|
|
stripeCustomerId: stripeCustomerId ?? this.stripeCustomerId,
|
|
|
|
|
stripeSubscriptionId: stripeSubscriptionId ?? this.stripeSubscriptionId,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
factory CompanyModel.empty() {
|
|
|
|
|
return const CompanyModel(
|
|
|
|
|
id: null,
|
|
|
|
|
createdAt: null,
|
|
|
|
|
userId: '',
|
|
|
|
|
ragioneSociale: '',
|
|
|
|
|
indirizzo: '',
|
|
|
|
|
cap: '',
|
|
|
|
|
citta: '',
|
|
|
|
|
provincia: '',
|
|
|
|
|
partitaIva: '',
|
|
|
|
|
codiceFiscale: '',
|
|
|
|
|
codiceUnivoco: '',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
factory CompanyModel.fromMap(Map<String, dynamic> map) {
|
2026-04-06 10:55:56 +02:00
|
|
|
return CompanyModel(
|
2026-04-22 11:06:02 +02:00
|
|
|
id: map['id'] as String?,
|
|
|
|
|
createdAt: map['created_at'] != null
|
|
|
|
|
? DateTime.tryParse(map['created_at'])
|
|
|
|
|
: null,
|
|
|
|
|
userId: map['user_id'] ?? '',
|
|
|
|
|
ragioneSociale: map['ragione_sociale'] ?? '',
|
|
|
|
|
indirizzo: map['indirizzo'] ?? '',
|
|
|
|
|
cap: map['cap'] ?? '',
|
|
|
|
|
citta: map['citta'] ?? '',
|
|
|
|
|
provincia: map['provincia'] ?? '',
|
|
|
|
|
partitaIva: map['partita_iva'] ?? '',
|
|
|
|
|
codiceFiscale: map['codice_fiscale'] ?? '',
|
|
|
|
|
codiceUnivoco: map['codice_univoco'] ?? '',
|
|
|
|
|
companyLogo: map['company_logo'] ?? '',
|
|
|
|
|
isPaid: map['is_paid'] ?? false,
|
|
|
|
|
paymentExpiration: map['payment_expiration'] != null
|
|
|
|
|
? DateTime.tryParse(map['payment_expiration'])
|
|
|
|
|
: null,
|
|
|
|
|
subscriptionTier: SubscriptionTier.fromString(map['subscription_tier']),
|
|
|
|
|
subscriptionStatus: SubscriptionStatus.fromString(
|
|
|
|
|
map['subscription_status'],
|
|
|
|
|
),
|
|
|
|
|
trialEndsAt: map['trial_ends_at'] != null
|
|
|
|
|
? DateTime.tryParse(map['trial_ends_at'])
|
2026-04-06 10:55:56 +02:00
|
|
|
: null,
|
2026-04-22 11:06:02 +02:00
|
|
|
stripeCustomerId: map['stripe_customer_id'],
|
|
|
|
|
stripeSubscriptionId: map['stripe_subscription_id'],
|
2026-04-06 10:55:56 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:06:02 +02:00
|
|
|
Map<String, dynamic> toMap() {
|
2026-04-06 10:55:56 +02:00
|
|
|
return {
|
2026-04-22 11:06:02 +02:00
|
|
|
if (id != null) 'id': id,
|
|
|
|
|
// created_at è gestito dal DB di default, di solito non si passa nell'insert
|
|
|
|
|
'user_id': userId,
|
2026-04-06 10:55:56 +02:00
|
|
|
'ragione_sociale': ragioneSociale,
|
|
|
|
|
'indirizzo': indirizzo,
|
|
|
|
|
'cap': cap,
|
|
|
|
|
'citta': citta,
|
|
|
|
|
'provincia': provincia,
|
|
|
|
|
'partita_iva': partitaIva,
|
|
|
|
|
'codice_fiscale': codiceFiscale,
|
|
|
|
|
'codice_univoco': codiceUnivoco,
|
2026-04-06 11:58:15 +02:00
|
|
|
'company_logo': companyLogo,
|
2026-04-22 11:06:02 +02:00
|
|
|
'is_paid': isPaid,
|
|
|
|
|
if (paymentExpiration != null)
|
|
|
|
|
'payment_expiration': paymentExpiration!.toIso8601String(),
|
|
|
|
|
'subscription_tier': subscriptionTier.name,
|
|
|
|
|
'subscription_status': subscriptionStatus == SubscriptionStatus.pastDue
|
|
|
|
|
? 'past_due'
|
|
|
|
|
: subscriptionStatus.name,
|
|
|
|
|
if (trialEndsAt != null) 'trial_ends_at': trialEndsAt!.toIso8601String(),
|
|
|
|
|
if (stripeCustomerId != null) 'stripe_customer_id': stripeCustomerId,
|
|
|
|
|
if (stripeSubscriptionId != null)
|
|
|
|
|
'stripe_subscription_id': stripeSubscriptionId,
|
2026-04-06 10:55:56 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
2026-04-22 11:06:02 +02:00
|
|
|
List<Object?> get props => [
|
|
|
|
|
id,
|
|
|
|
|
createdAt,
|
|
|
|
|
userId,
|
|
|
|
|
ragioneSociale,
|
|
|
|
|
indirizzo,
|
|
|
|
|
cap,
|
|
|
|
|
citta,
|
|
|
|
|
provincia,
|
|
|
|
|
partitaIva,
|
|
|
|
|
codiceFiscale,
|
|
|
|
|
codiceUnivoco,
|
|
|
|
|
companyLogo,
|
|
|
|
|
isPaid,
|
|
|
|
|
paymentExpiration,
|
|
|
|
|
subscriptionTier,
|
|
|
|
|
subscriptionStatus,
|
|
|
|
|
trialEndsAt,
|
|
|
|
|
stripeCustomerId,
|
|
|
|
|
stripeSubscriptionId,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================================================================
|
|
|
|
|
// BUSINESS LOGIC: I LIMITI DEI TIER
|
|
|
|
|
// ===================================================================
|
|
|
|
|
|
|
|
|
|
extension CompanyLimits on CompanyModel {
|
|
|
|
|
int get maxStores {
|
|
|
|
|
switch (subscriptionTier) {
|
|
|
|
|
case SubscriptionTier.free:
|
|
|
|
|
return 1;
|
|
|
|
|
case SubscriptionTier.pro:
|
|
|
|
|
return 1;
|
|
|
|
|
case SubscriptionTier.premium:
|
|
|
|
|
return 10;
|
|
|
|
|
case SubscriptionTier.platinum:
|
|
|
|
|
return 30;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int get maxStaffMembers {
|
|
|
|
|
switch (subscriptionTier) {
|
|
|
|
|
case SubscriptionTier.free:
|
|
|
|
|
return 2;
|
|
|
|
|
case SubscriptionTier.pro:
|
|
|
|
|
return 10;
|
|
|
|
|
case SubscriptionTier.premium:
|
|
|
|
|
return 50;
|
|
|
|
|
case SubscriptionTier.platinum:
|
|
|
|
|
return 150;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 10:11:44 +02:00
|
|
|
int get maxOperationsPerMonth {
|
2026-04-22 11:06:02 +02:00
|
|
|
switch (subscriptionTier) {
|
|
|
|
|
case SubscriptionTier.free:
|
|
|
|
|
return 50;
|
|
|
|
|
case SubscriptionTier.pro:
|
|
|
|
|
return 1000;
|
|
|
|
|
case SubscriptionTier.premium:
|
|
|
|
|
return 10000;
|
|
|
|
|
case SubscriptionTier.platinum:
|
|
|
|
|
return 30000;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int get maxStorageGb {
|
|
|
|
|
switch (subscriptionTier) {
|
|
|
|
|
case SubscriptionTier.free:
|
|
|
|
|
return 0; // 500MB = 0.5 GB, magari qui usi i MB interi
|
|
|
|
|
case SubscriptionTier.pro:
|
|
|
|
|
return 3;
|
|
|
|
|
case SubscriptionTier.premium:
|
|
|
|
|
return 30;
|
|
|
|
|
case SubscriptionTier.platinum:
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verifica generale: L'utente ha i permessi per usare l'app?
|
|
|
|
|
bool get hasActiveAccess {
|
|
|
|
|
// 1. Priorità all'override manuale (is_paid e payment_expiration)
|
|
|
|
|
if (isPaid) {
|
2026-04-26 10:15:34 +02:00
|
|
|
if (paymentExpiration == null) {
|
2026-04-22 11:06:02 +02:00
|
|
|
return true; // Pagato "a vita" o senza scadenza
|
2026-04-26 10:15:34 +02:00
|
|
|
}
|
2026-04-22 11:06:02 +02:00
|
|
|
if (DateTime.now().isBefore(paymentExpiration!)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Controllo SaaS (Stripe/Subscription)
|
|
|
|
|
if (subscriptionStatus == SubscriptionStatus.active) return true;
|
|
|
|
|
|
|
|
|
|
// 3. Controllo Trial
|
|
|
|
|
if (subscriptionStatus == SubscriptionStatus.trialing &&
|
|
|
|
|
trialEndsAt != null) {
|
|
|
|
|
return DateTime.now().isBefore(trialEndsAt!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scaduto, past_due, o cancellato
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-06 10:55:56 +02:00
|
|
|
}
|