pending = null; if (isset($values[FinanceHelper::FORM_MARKER])) { $this->pending = array( 'project_id' => isset($values['project_id']) ? (int) $values['project_id'] : 0, 'fields' => $this->extract($values), ); } $this->strip($values); } /** * model:task:creation:aftersave -- fires after INSERT, hands over the new task id by reference. */ public function onCreationAfterSave(&$task_id) { if ($this->pending !== null) { $this->persist((int) $task_id, $this->pending['project_id'], $this->pending['fields']); $this->pending = null; return; } // Not a form submission. Native recurrence builds the next occurrence with a plain task // create (which copies no metadata) after setting recurrence_parent to the source task, so // carry the finance metadata across -- otherwise the recurring card loses its money. $task = $this->taskFinderModel->getById((int) $task_id); if (! empty($task['recurrence_parent'])) { // A recurrence is the NEXT occurrence, so advance the installment (p2/30 -> p3/30). $this->copyMetadata((int) $task['recurrence_parent'], (int) $task_id, true); } } /** * model:task:duplication:aftersave -- the manual "Duplicate" action. TaskDuplicationModel does * not copy metadata, so carry the finance fields to the new task. */ public function onDuplication(array &$hook_values) { if (! empty($hook_values['source_task_id']) && ! empty($hook_values['destination_task_id'])) { $this->copyMetadata((int) $hook_values['source_task_id'], (int) $hook_values['destination_task_id']); } } /** * recoreco:task:spawned -- RecoReco (calendar recurrence) just spawned a copy. Its ongoing card * is the TEMPLATE that stays (unlike native recurrence, whose ongoing card is the new child), so * advance the SOURCE template's installment. The clone already copied the current installment * verbatim via onDuplication during RecoReco's duplicate() call. */ public function onRecoRecoSpawn(array &$payload) { if (! empty($payload['source_task_id'])) { $this->advanceInstallment((int) $payload['source_task_id']); } } /** * model:task:modification:prepare -- fires before UPDATE, $values by reference. The task id is * removed from $values by core, but it is the route parameter of the modification request. */ public function onModificationPrepare(array &$values) { if (isset($values[FinanceHelper::FORM_MARKER])) { $task_id = $this->request->getIntegerParam('task_id'); $project_id = isset($values['project_id']) ? (int) $values['project_id'] : 0; if ($task_id > 0) { $this->persist($task_id, $project_id, $this->extract($values)); } } $this->strip($values); } private function extract(array $values) { return array( 'amount' => isset($values[FinanceHelper::AMOUNT_KEY]) ? $values[FinanceHelper::AMOUNT_KEY] : '', 'direction' => (isset($values[FinanceHelper::DIRECTION_KEY]) && $values[FinanceHelper::DIRECTION_KEY] === 'credit') ? 'credit' : 'debit', 'installment_current' => isset($values[FinanceHelper::INSTALLMENT_CURRENT_KEY]) ? $values[FinanceHelper::INSTALLMENT_CURRENT_KEY] : '', 'installment_total' => isset($values[FinanceHelper::INSTALLMENT_TOTAL_KEY]) ? $values[FinanceHelper::INSTALLMENT_TOTAL_KEY] : '', ); } private function strip(array &$values) { foreach (array_keys($values) as $key) { if (strpos($key, 'financebuddy_') === 0) { unset($values[$key]); } } } private function persist($task_id, $project_id, array $fields) { // Only touch metadata on boards where a manager opted in. if ((int) $this->projectMetadataModel->get($project_id, FinanceHelper::ENABLED_KEY, 0) !== 1) { return; } $amount = FinanceHelper::parseAmount($fields['amount']); // No (or cleared) amount: drop any finance metadata so the card goes back to plain. if ($amount <= 0) { foreach (array(FinanceHelper::AMOUNT_KEY, FinanceHelper::DIRECTION_KEY, FinanceHelper::INSTALLMENT_CURRENT_KEY, FinanceHelper::INSTALLMENT_TOTAL_KEY) as $key) { $this->taskMetadataModel->remove($task_id, $key); } return; } $this->taskMetadataModel->save($task_id, array( FinanceHelper::AMOUNT_KEY => number_format($amount, 2, '.', ''), FinanceHelper::DIRECTION_KEY => $fields['direction'] === 'credit' ? 'credit' : 'debit', FinanceHelper::INSTALLMENT_CURRENT_KEY => $this->intOrBlank($fields['installment_current']), FinanceHelper::INSTALLMENT_TOTAL_KEY => $this->intOrBlank($fields['installment_total']), )); } /** * Copy the finance metadata from one task to another (recurrence next occurrence, or a manual * duplicate). When $advance is true, bump the current installment by one -- clamped at the * total so a finite plan stops at its last installment (e.g. p30/30 stays p30/30). */ private function copyMetadata($source_id, $dest_id, $advance = false) { if ($source_id <= 0 || $dest_id <= 0) { return; } $amount = $this->taskMetadataModel->get($source_id, FinanceHelper::AMOUNT_KEY, ''); // Nothing to carry over if the source has no money set. if ($amount === '' || (float) $amount <= 0) { return; } $direction = $this->taskMetadataModel->get($source_id, FinanceHelper::DIRECTION_KEY, 'debit'); $current = $this->taskMetadataModel->get($source_id, FinanceHelper::INSTALLMENT_CURRENT_KEY, ''); $total = $this->taskMetadataModel->get($source_id, FinanceHelper::INSTALLMENT_TOTAL_KEY, ''); if ($advance && $current !== '' && ctype_digit((string) $current)) { $next = (int) $current + 1; if ($total !== '' && ctype_digit((string) $total) && $next > (int) $total) { $next = (int) $total; } $current = (string) $next; } $this->taskMetadataModel->save($dest_id, array( FinanceHelper::AMOUNT_KEY => $amount, FinanceHelper::DIRECTION_KEY => $direction === 'credit' ? 'credit' : 'debit', FinanceHelper::INSTALLMENT_CURRENT_KEY => $current, FinanceHelper::INSTALLMENT_TOTAL_KEY => $total, )); } /** * Advance a task's current installment by one, in place. A finite plan clamps at the total * (p10/10 stays p10/10); an open-ended one (no total) increments unbounded (p2 -> p3). Cards * with no numeric current installment are left alone. */ private function advanceInstallment($task_id) { $current = $this->taskMetadataModel->get($task_id, FinanceHelper::INSTALLMENT_CURRENT_KEY, ''); if ($current === '' || ! ctype_digit((string) $current)) { return; } $total = $this->taskMetadataModel->get($task_id, FinanceHelper::INSTALLMENT_TOTAL_KEY, ''); $next = (int) $current + 1; if ($total !== '' && ctype_digit((string) $total) && $next > (int) $total) { $next = (int) $total; } $this->taskMetadataModel->save($task_id, array( FinanceHelper::INSTALLMENT_CURRENT_KEY => (string) $next, )); } private function intOrBlank($raw) { $raw = trim((string) $raw); return ($raw !== '' && ctype_digit($raw) && (int) $raw > 0) ? (string) (int) $raw : ''; } }