185 lines
6.3 KiB
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;
|
|
}
|
|
}
|