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:
@@ -9,11 +9,11 @@ class CompanyRepository {
|
||||
// .select().single() trasforma la risposta nell'oggetto appena inserito
|
||||
final response = await _supabase
|
||||
.from('company')
|
||||
.insert(company.toJson())
|
||||
.insert(company.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return CompanyModel.fromJson(response);
|
||||
return CompanyModel.fromMap(response);
|
||||
} on PostgrestException catch (e) {
|
||||
throw e.message;
|
||||
} catch (e) {
|
||||
@@ -30,7 +30,7 @@ class CompanyRepository {
|
||||
.eq('user_id', userId as Object)
|
||||
.maybeSingle();
|
||||
|
||||
return response != null ? CompanyModel.fromJson(response) : null;
|
||||
return response != null ? CompanyModel.fromMap(response) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/auth/bloc/auth_bloc.dart';
|
||||
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
||||
import 'package:flux/features/company/bloc/company_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
@@ -46,7 +46,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
||||
void _onSave() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc
|
||||
final userId = context.read<SessionBloc>().state.userId!;
|
||||
final userId = context.read<SessionCubit>().state.user!.id;
|
||||
|
||||
final company = CompanyModel(
|
||||
userId: userId,
|
||||
@@ -77,7 +77,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
||||
onPressed: () {
|
||||
// Qui chiami il tuo Bloc dell'autenticazione per fare logout
|
||||
// Esempio se hai un AuthBloc o SessionBloc:
|
||||
context.read<AuthBloc>().add(LogoutRequested());
|
||||
//context.read<AuthBloc>().add(LogoutRequested());
|
||||
|
||||
// Se vuoi solo tornare brutalmente alla login per testare il logo:
|
||||
// Navigator.of(context).pushReplacementNamed('/login');
|
||||
@@ -92,7 +92,7 @@ class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
||||
//GetIt.I.get<AppSettings>().setCurrentCompany(state.company);
|
||||
|
||||
// 2. Notifichiamo il SessionBloc per cambiare pagina
|
||||
context.read<SessionBloc>().add(AppStarted());
|
||||
//context.read<SessionCubit>().add(AppStarted());
|
||||
}
|
||||
|
||||
if (state.status == CompanyStatus.failure) {
|
||||
|
||||
Reference in New Issue
Block a user