import 'package:drift/drift.dart'; import '../database/database.dart'; /// Clones a month into a new month in a single transaction. /// /// Clone rules: /// - opening_balance_cents = 0 (NOT carried from previous) /// - buffer from settings /// - BillDefinitions: cloned, amount_override reset, is_paid=false, paymentMode preserved /// - PaymentRules: cloned with bill ID mapping /// - PaycheckDefinitions: cloned, dates adjusted to new month (day clamped) /// - PayPeriodItems: cloned as templates with zeroed amounts /// - eating_out_budget_cents: cloned from source /// - Payouts: NOT cloned (ephemeral) /// - MiscItems (recurring): cloned with is_enabled=true /// - MiscItems (non-recurring): NOT cloned Future cloneMonth({ required AppDatabase db, required int sourceMonthPlanId, required int targetYear, required int targetMonth, }) async { return db.transaction(() async { final source = await (db.select(db.monthPlans) ..where((t) => t.id.equals(sourceMonthPlanId))) .getSingle(); final settings = await (db.select(db.appSettings) ..where((t) => t.id.equals(1))) .getSingle(); final newMonthId = await db.into(db.monthPlans).insert( MonthPlansCompanion.insert( year: targetYear, month: targetMonth, openingBalanceCents: const Value(0), bufferAmountCents: Value(settings.defaultBufferCents), eatingOutBudgetCents: Value(source.eatingOutBudgetCents), clonedFromId: Value(sourceMonthPlanId), ), ); // Clone BillDefinitions — track old ID → new ID for rule mapping final sourceBills = await (db.select(db.billDefinitions) ..where((t) => t.monthPlanId.equals(sourceMonthPlanId)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); final billIdMap = {}; // old bill id → new bill id for (final bill in sourceBills) { final newBillId = await db.into(db.billDefinitions).insert( BillDefinitionsCompanion.insert( monthPlanId: newMonthId, name: bill.name, subtitle: Value(bill.subtitle), dueDay: bill.dueDay, defaultAmountCents: bill.defaultAmountCents, amountOverrideCents: const Value(null), // reset priority: Value(bill.priority), isAutoPay: Value(bill.isAutoPay), isFreeform: Value(bill.isFreeform), outstandingBalanceCents: Value(bill.outstandingBalanceCents), isPaid: const Value(false), // reset isOutside: Value(bill.isOutside), balanceType: Value(bill.balanceType), paymentMode: Value(bill.paymentMode), sortOrder: bill.sortOrder, ), ); billIdMap[bill.id] = newBillId; } // Clone PaymentRules with bill ID mapping final sourceRules = await (db.select(db.paymentRules) ..where((t) => t.monthPlanId.equals(sourceMonthPlanId)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); for (final rule in sourceRules) { final newBillId = billIdMap[rule.billDefinitionId]; if (newBillId == null) continue; // bill was deleted await db.into(db.paymentRules).insert( PaymentRulesCompanion.insert( monthPlanId: newMonthId, billDefinitionId: newBillId, mathSource: rule.mathSource, ruleType: rule.ruleType, minimumAmountCents: Value(rule.minimumAmountCents), multiplier: Value(rule.multiplier), additionCents: Value(rule.additionCents), sortOrder: rule.sortOrder, ), ); } // Clone PaycheckDefinitions (adjust dates) final sourcePaychecks = await (db.select(db.paycheckDefinitions) ..where((t) => t.monthPlanId.equals(sourceMonthPlanId)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); for (final pc in sourcePaychecks) { final lastDay = DateTime(targetYear, targetMonth + 1, 0).day; final clampedDay = pc.date.day > lastDay ? lastDay : pc.date.day; final newDate = DateTime(targetYear, targetMonth, clampedDay); final newPcId = await db.into(db.paycheckDefinitions).insert( PaycheckDefinitionsCompanion.insert( monthPlanId: newMonthId, sequence: pc.sequence, date: newDate, amountCents: pc.amountCents, sortOrder: pc.sortOrder, ), ); final sourceItems = await (db.select(db.payPeriodItems) ..where((t) => t.paycheckId.equals(pc.id)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); for (final item in sourceItems) { final itemDate = DateTime(targetYear, targetMonth, clampedDay); await db.into(db.payPeriodItems).insert( PayPeriodItemsCompanion.insert( paycheckId: newPcId, description: item.description, amountCents: 0, date: itemDate, sortOrder: item.sortOrder, ), ); } } // Clone recurring MiscItems final sourceMisc = await (db.select(db.miscItems) ..where((t) => t.monthPlanId.equals(sourceMonthPlanId) & t.isRecurring.equals(true)) ..orderBy([(t) => OrderingTerm.asc(t.sortOrder)])) .get(); for (final item in sourceMisc) { await db.into(db.miscItems).insert( MiscItemsCompanion.insert( monthPlanId: newMonthId, description: item.description, comment: Value(item.comment), amountCents: item.amountCents, date: Value(item.date), isRecurring: const Value(true), isEnabled: const Value(true), sortOrder: item.sortOrder, ), ); } return newMonthId; }); }