feat: improve patient flow and pricing workflow
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user