rework-onboarding (#7)

Onboarding completato, ora super rapido e top

Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/7
Co-authored-by: Mark M2 Macbook <marco@catelli.it>
Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
2026-04-22 11:06:02 +02:00
committed by brontomark
parent c5b5b76bd6
commit 90bd5ecacf
47 changed files with 1742 additions and 516 deletions

View File

@@ -1,9 +1,50 @@
import 'package:equatable/equatable.dart';
// ===================================================================
// 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
// ===================================================================
class CompanyModel extends Equatable {
final String id;
final String? id;
final DateTime? createdAt;
final String userId;
final String userId; // Nel DB è user_id (chiave esterna su auth.users)
// Dati Anagrafici e Fatturazione
final String ragioneSociale;
final String indirizzo;
final String cap;
@@ -12,12 +53,21 @@ class CompanyModel extends Equatable {
final String partitaIva;
final String codiceFiscale;
final String codiceUnivoco;
final bool isPaid;
final DateTime? paymentExpiration;
final String companyLogo;
// Stato Pagamenti (Ibride: manuale + Stripe)
final bool isPaid;
final DateTime? paymentExpiration;
// Campi SaaS Stripe/Automazioni
final SubscriptionTier subscriptionTier;
final SubscriptionStatus subscriptionStatus;
final DateTime? trialEndsAt;
final String? stripeCustomerId;
final String? stripeSubscriptionId;
const CompanyModel({
this.id = '',
this.id,
this.createdAt,
required this.userId,
required this.ragioneSociale,
@@ -28,48 +78,16 @@ class CompanyModel extends Equatable {
required this.partitaIva,
required this.codiceFiscale,
required this.codiceUnivoco,
this.companyLogo = '',
this.isPaid = false,
this.paymentExpiration,
this.companyLogo = '',
this.subscriptionTier = SubscriptionTier.free,
this.subscriptionStatus = SubscriptionStatus.trialing,
this.trialEndsAt,
this.stripeCustomerId,
this.stripeSubscriptionId,
});
factory CompanyModel.fromJson(Map<String, dynamic> json) {
return CompanyModel(
id: json['id'] as String,
createdAt: DateTime.parse(json['created_at']),
userId: json['user_id'] as String,
ragioneSociale: json['ragione_sociale'],
indirizzo: json['indirizzo'],
cap: json['cap'],
citta: json['citta'],
provincia: json['provincia'],
partitaIva: json['partita_iva'],
codiceFiscale: json['codice_fiscale'],
codiceUnivoco: json['codice_univoco'],
isPaid: json['is_paid'] ?? false,
paymentExpiration: json['payment_expiration'] != null
? DateTime.parse(json['payment_expiration'])
: null,
companyLogo: json['company_logo'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'ragione_sociale': ragioneSociale,
'indirizzo': indirizzo,
'cap': cap,
'citta': citta,
'provincia': provincia,
'partita_iva': partitaIva,
'codice_fiscale': codiceFiscale,
'codice_univoco': codiceUnivoco,
'is_paid': isPaid,
'payment_expiration': paymentExpiration?.toIso8601String(),
'company_logo': companyLogo,
};
}
CompanyModel copyWith({
String? id,
DateTime? createdAt,
@@ -82,26 +100,214 @@ class CompanyModel extends Equatable {
String? partitaIva,
String? codiceFiscale,
String? codiceUnivoco,
String? companyLogo,
bool? isPaid,
DateTime? paymentExpiration,
String? companyLogo,
}) => 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,
isPaid: isPaid ?? this.isPaid,
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
companyLogo: companyLogo ?? this.companyLogo,
);
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) {
return CompanyModel(
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'])
: null,
stripeCustomerId: map['stripe_customer_id'],
stripeSubscriptionId: map['stripe_subscription_id'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
// created_at è gestito dal DB di default, di solito non si passa nell'insert
'user_id': userId,
'ragione_sociale': ragioneSociale,
'indirizzo': indirizzo,
'cap': cap,
'citta': citta,
'provincia': provincia,
'partita_iva': partitaIva,
'codice_fiscale': codiceFiscale,
'codice_univoco': codiceUnivoco,
'company_logo': companyLogo,
'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,
};
}
@override
List<Object?> get props => [id, userId, ragioneSociale, partitaIva, isPaid];
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;
}
}
int get maxServicesPerMonth {
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) {
if (paymentExpiration == null)
return true; // Pagato "a vita" o senza scadenza
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;
}
}