ultimatepos/Modules/Accounting/Console/AccountingBackfillCommand.php

185 lines
6.3 KiB
PHP

<?php
namespace Modules\Accounting\Console;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AccountingBackfillCommand extends Command
{
protected $signature = 'accounting:backfill
{--business_id= : Business ID}
{--link-payment-accounts : Link payment accounts to GL by name match}
{--posting-keys : Backfill posting_key on GL lines from posting logs}
{--entity-tags : Backfill holding_entity_id from location mappings}
{--auto-map : Auto-map recent unmapped transactions using location defaults}';
protected $description = 'Backfill accounting integration data for holding rollout';
public function handle(): int
{
$businessId = $this->option('business_id');
if ($this->option('posting-keys')) {
$this->backfillPostingKeys($businessId);
}
if ($this->option('link-payment-accounts')) {
$this->linkPaymentAccounts($businessId);
}
if ($this->option('entity-tags')) {
$this->backfillEntityTags($businessId);
}
if ($this->option('auto-map')) {
$this->autoMapTransactions($businessId);
}
if (! $this->option('posting-keys') && ! $this->option('link-payment-accounts') && ! $this->option('entity-tags') && ! $this->option('auto-map')) {
$this->backfillPostingKeys($businessId);
$this->linkPaymentAccounts($businessId);
$this->backfillEntityTags($businessId);
}
return self::SUCCESS;
}
protected function backfillPostingKeys(?string $businessId): void
{
if (! Schema::hasTable('accounting_posting_logs') || ! Schema::hasColumn('accounting_accounts_transactions', 'posting_key')) {
$this->warn('Posting log tables not available.');
return;
}
$logs = DB::table('accounting_posting_logs')->where('status', 'posted');
if ($businessId) {
$logs->where('business_id', $businessId);
}
$count = 0;
foreach ($logs->get() as $log) {
if (! $log->acc_trans_mapping_id) {
continue;
}
$updated = DB::table('accounting_accounts_transactions')
->where('acc_trans_mapping_id', $log->acc_trans_mapping_id)
->whereNull('posting_key')
->update([
'posting_key' => $log->posting_key,
'source_type' => $log->source_type,
'source_id' => $log->source_id,
]);
$count += $updated;
}
$this->info("Backfilled posting_key on {$count} GL line(s).");
}
protected function linkPaymentAccounts(?string $businessId): void
{
if (! Schema::hasColumn('accounts', 'accounting_account_id')) {
$this->warn('accounts.accounting_account_id column not available.');
return;
}
$query = DB::table('business');
if ($businessId) {
$query->where('id', $businessId);
}
$service = app(\Modules\Accounting\Services\PaymentAccountGlLinkService::class);
$linked = 0;
foreach ($query->pluck('id') as $id) {
$linked += $service->linkByNameMatch((int) $id);
}
$this->info("Linked {$linked} payment account(s) to GL by name match.");
}
protected function backfillEntityTags(?string $businessId): void
{
if (! class_exists(\Modules\Accounting\Services\HoldingEntityResolutionService::class)) {
return;
}
$service = app(\Modules\Accounting\Services\HoldingEntityResolutionService::class);
$query = DB::table('business');
if ($businessId) {
$query->where('id', $businessId);
}
$total = 0;
foreach ($query->pluck('id') as $id) {
$total += $service->backfillUntagged((int) $id);
}
$this->info("Backfilled holding_entity_id on {$total} GL line(s).");
}
protected function autoMapTransactions(?string $businessId): void
{
if (! class_exists(\Modules\Accounting\Services\OperationalTransactionPostingService::class)) {
return;
}
$service = app(\Modules\Accounting\Services\OperationalTransactionPostingService::class);
$query = DB::table('business');
if ($businessId) {
$query->where('id', $businessId);
}
$mapped = 0;
foreach ($query->pluck('id') as $business) {
$mapped += $this->mapRecentTransactionsForBusiness((int) $business, $service);
}
$this->info("Auto-mapped {$mapped} operational transaction(s).");
}
protected function mapRecentTransactionsForBusiness(int $businessId, \Modules\Accounting\Services\OperationalTransactionPostingService $service): int
{
$since = now()->subDays(90)->toDateString();
$count = 0;
$userId = 1;
$transactions = DB::table('transactions')
->where('business_id', $businessId)
->whereIn('type', ['sell', 'purchase', 'expense'])
->where('status', 'final')
->whereDate('transaction_date', '>=', $since)
->get(['id', 'type', 'location_id']);
foreach ($transactions as $transaction) {
if (DB::table('accounting_accounts_transactions')->where('transaction_id', $transaction->id)->exists()) {
continue;
}
$location = DB::table('business_locations')->where('id', $transaction->location_id)->first();
if (! $location) {
continue;
}
$map = json_decode($location->accounting_default_map ?? '{}', true);
$scenario = $transaction->type === 'sell' ? 'sale' : ($transaction->type === 'purchase' ? 'purchases' : 'expense');
$depositTo = $map[$scenario]['deposit_to'] ?? null;
$paymentAccount = $map[$scenario]['payment_account'] ?? null;
if (! $depositTo || ! $paymentAccount) {
continue;
}
$type = $transaction->type === 'sell' ? 'sell' : $transaction->type;
$service->post($type, (int) $transaction->id, $userId, $businessId, (int) $depositTo, (int) $paymentAccount, (int) $transaction->location_id);
$count++;
}
return $count;
}
}