feat: improve patient flow and pricing workflow

This commit is contained in:
egecankomur
2026-06-12 00:04:53 +03:00
parent e12587398b
commit b42f68214e
26 changed files with 1283 additions and 243 deletions
+117
View File
@@ -0,0 +1,117 @@
import 'package:pocketbase/pocketbase.dart';
import '../api/pocketbase_client.dart';
class FinanceService {
FinanceService._();
static final instance = FinanceService._();
PocketBase get _pb => PocketBaseClient.instance.pb;
Future<void> ensureEntriesForJob({
required String jobId,
required String clinicTenantId,
required String labTenantId,
required String clinicName,
required String labName,
required double amount,
required String currency,
}) async {
if (amount <= 0) return;
final existing = await _pb.collection('finance_entries').getFullList(
filter: 'job_id = "$jobId"',
batch: 200,
);
await _upsertEntry(
existing: existing,
jobId: jobId,
tenantId: clinicTenantId,
counterpartyTenantId: labTenantId,
counterpartyName: labName,
type: 'payable',
amount: amount,
currency: currency,
);
await _upsertEntry(
existing: existing,
jobId: jobId,
tenantId: labTenantId,
counterpartyTenantId: clinicTenantId,
counterpartyName: clinicName,
type: 'receivable',
amount: amount,
currency: currency,
);
}
Future<void> markJobPaid(String jobId) async {
final existing = await _pb.collection('finance_entries').getFullList(
filter: 'job_id = "$jobId"',
batch: 200,
);
final paidAt = DateTime.now().toIso8601String();
for (final record in existing) {
await _pb.collection('finance_entries').update(
record.id,
body: {
'status': 'paid',
'paid_at': paidAt,
},
);
}
}
Future<void> deletePendingEntriesForJob(String jobId) async {
final existing = await _pb.collection('finance_entries').getFullList(
filter: 'job_id = "$jobId" && status = "pending"',
batch: 200,
);
for (final record in existing) {
await _pb.collection('finance_entries').delete(record.id);
}
}
Future<void> _upsertEntry({
required List<RecordModel> existing,
required String jobId,
required String tenantId,
required String counterpartyTenantId,
required String counterpartyName,
required String type,
required double amount,
required String currency,
}) async {
RecordModel? match;
try {
match = existing.firstWhere(
(record) =>
record.data['tenant_id'] == tenantId &&
record.data['type'] == type,
);
} catch (_) {
match = null;
}
final body = {
'tenant_id': tenantId,
'job_id': jobId,
'type': type,
'amount': amount,
'currency': currency,
'status': 'pending',
'paid_at': null,
'counterparty_tenant_id': counterpartyTenantId,
'counterparty_name': counterpartyName,
};
if (match == null) {
await _pb.collection('finance_entries').create(body: body);
return;
}
await _pb.collection('finance_entries').update(match.id, body: body);
}
}
+87
View File
@@ -0,0 +1,87 @@
import '../../models/clinic_discount.dart';
import '../../models/job.dart';
import '../../models/prosthetic_product.dart';
class PricingBreakdown {
const PricingBreakdown({
required this.billableUnits,
required this.unitPrice,
required this.baseAmount,
required this.discountAmount,
required this.finalAmount,
required this.appliedDiscounts,
});
final int billableUnits;
final double unitPrice;
final double baseAmount;
final double discountAmount;
final double finalAmount;
final List<ClinicDiscount> appliedDiscounts;
}
class PricingService {
PricingService._();
static final instance = PricingService._();
int billableUnitsForType(ProstheticType type, int memberCount) {
final safeCount = memberCount <= 0 ? 1 : memberCount;
return switch (type) {
ProstheticType.tamProtez || ProstheticType.parsiyel => 1,
_ => safeCount,
};
}
String unitLabelForType(ProstheticType type) {
return switch (type) {
ProstheticType.tamProtez || ProstheticType.parsiyel => 'vaka',
_ => 'diş',
};
}
PricingBreakdown calculate({
required ProstheticProduct product,
required ProstheticType prostheticType,
required int memberCount,
required String clinicTenantId,
required List<ClinicDiscount> discounts,
}) {
final billableUnits = billableUnitsForType(prostheticType, memberCount);
final unitPrice = product.unitPrice ?? 0;
final baseAmount = unitPrice * billableUnits;
final applicable = discounts.where((discount) {
if (!discount.isActive) return false;
if (!(discount.appliesToAll || discount.clinicTenantId == clinicTenantId)) {
return false;
}
if (!(discount.appliesToAllTypes ||
discount.prostheticType == prostheticType.value)) {
return false;
}
if (discount.minQuantity > 0 && billableUnits < discount.minQuantity) {
return false;
}
return true;
}).toList();
double running = baseAmount;
for (final discount in applicable) {
running = discount.discountType == DiscountType.percentage
? running * (1 - discount.discountValue / 100)
: running - discount.discountValue;
}
final finalAmount = running.clamp(0, double.infinity).toDouble();
return PricingBreakdown(
billableUnits: billableUnits,
unitPrice: unitPrice,
baseAmount: baseAmount,
discountAmount: (baseAmount - finalAmount)
.clamp(0, double.infinity)
.toDouble(),
finalAmount: finalAmount,
appliedDiscounts: applicable,
);
}
}