diff --git a/app/Casts/InvoiceSyncCast.php b/app/Casts/InvoiceSyncCast.php index 18cfbe627e..bc5f6cfcc6 100644 --- a/app/Casts/InvoiceSyncCast.php +++ b/app/Casts/InvoiceSyncCast.php @@ -19,7 +19,6 @@ class InvoiceSyncCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { - if (is_null($value)) { return null; // Return null if the value is null } @@ -37,21 +36,21 @@ class InvoiceSyncCast implements CastsAttributes public function set($model, string $key, $value, array $attributes) { - - - if (is_null($value)) { return [$key => null]; } - - - return [ - $key => json_encode([ - 'qb_id' => $value->qb_id, - ]) + $data = [ + 'qb_id' => $value->qb_id, ]; + // Handle structured nested object + if ($value->tax_report !== null) { + $data['tax_report'] = $value->tax_report->toArray(); + } + return [ + $key => json_encode($data) + ]; } } diff --git a/app/DataMapper/InvoiceSync.php b/app/DataMapper/InvoiceSync.php index 49cc4d4cc7..c4111d5ed0 100644 --- a/app/DataMapper/InvoiceSync.php +++ b/app/DataMapper/InvoiceSync.php @@ -21,12 +21,14 @@ use Illuminate\Contracts\Database\Eloquent\Castable; class InvoiceSync implements Castable { public string $qb_id; + public ?TaxReport $tax_report; // Structured nested object public function __construct(array $attributes = []) { - $this->qb_id = $attributes['qb_id'] ?? ''; - + $this->tax_report = isset($attributes['tax_report']) + ? new TaxReport($attributes['tax_report']) + : null; // Handle structured nested object } /** @@ -44,3 +46,256 @@ class InvoiceSync implements Castable return new self($data); } } + +/** + * Tax report object for InvoiceSync - tracks incremental tax history + */ +class TaxReport +{ + public string $nexus; + public string $country_nexus; + public string $report_period; // e.g., "2024-Q1", "2024-01" + public string $last_updated; + public ?array $tax_summary; // Summary totals + public ?array $tax_details; // Array of TaxDetail objects + public ?array $tax_adjustments; // Array of TaxAdjustment objects + + public function __construct(array $attributes = []) + { + $this->nexus = $attributes['nexus'] ?? ''; + $this->country_nexus = $attributes['country_nexus'] ?? ''; + $this->report_period = $attributes['report_period'] ?? ''; + $this->last_updated = $attributes['last_updated'] ?? ''; + $this->tax_summary = isset($attributes['tax_summary']) + ? new TaxSummary($attributes['tax_summary']) + : null; + $this->tax_details = isset($attributes['tax_details']) + ? array_map(fn($detail) => new TaxDetail($detail), $attributes['tax_details']) + : null; + $this->tax_adjustments = isset($attributes['tax_adjustments']) + ? array_map(fn($adjustment) => new TaxAdjustment($adjustment), $attributes['tax_adjustments']) + : null; + } + + public function toArray(): array + { + return [ + 'nexus' => $this->nexus, + 'country_nexus' => $this->country_nexus, + 'report_period' => $this->report_period, + 'last_updated' => $this->last_updated, + 'tax_summary' => $this->tax_summary?->toArray(), + 'tax_details' => $this->tax_details ? array_map(fn($detail) => $detail->toArray(), $this->tax_details) : null, + 'tax_adjustments' => $this->tax_adjustments ? array_map(fn($adjustment) => $adjustment->toArray(), $this->tax_adjustments) : null, + ]; + } +} + +/** + * Tax summary with totals for different tax states + */ +class TaxSummary +{ + public float $total_collected; // Tax collected and confirmed + public float $total_pending; // Tax pending collection + public float $total_refundable; // Tax that needs to be claimed back + public float $total_partially_paid; // Tax partially paid + public float $total_adjustments; // Net adjustments + public float $net_tax_liability; // Final tax liability + public ?array $period_totals; // Totals by report period + + public function __construct(array $attributes = []) + { + $this->total_collected = $attributes['total_collected'] ?? 0.0; + $this->total_pending = $attributes['total_pending'] ?? 0.0; + $this->total_refundable = $attributes['total_refundable'] ?? 0.0; + $this->total_partially_paid = $attributes['total_partially_paid'] ?? 0.0; + $this->total_adjustments = $attributes['total_adjustments'] ?? 0.0; + $this->net_tax_liability = $attributes['net_tax_liability'] ?? 0.0; + $this->period_totals = isset($attributes['period_totals']) + ? array_map(fn($period) => new PeriodTotal($period), $attributes['period_totals']) + : null; + } + + public function toArray(): array + { + return [ + 'total_collected' => $this->total_collected, + 'total_pending' => $this->total_pending, + 'total_refundable' => $this->total_refundable, + 'total_partially_paid' => $this->total_partially_paid, + 'total_adjustments' => $this->total_adjustments, + 'net_tax_liability' => $this->net_tax_liability, + 'period_totals' => $this->period_totals ? array_map(fn($period) => $period->toArray(), $this->period_totals) : null, + ]; + } +} + +/** + * Period-specific tax totals + */ +class PeriodTotal +{ + public string $report_period; + public float $collected_in_period; + public float $pending_in_period; + public float $refundable_in_period; + public float $adjustments_in_period; + + public function __construct(array $attributes = []) + { + $this->report_period = $attributes['report_period'] ?? ''; + $this->collected_in_period = $attributes['collected_in_period'] ?? 0.0; + $this->pending_in_period = $attributes['pending_in_period'] ?? 0.0; + $this->refundable_in_period = $attributes['refundable_in_period'] ?? 0.0; + $this->adjustments_in_period = $attributes['adjustments_in_period'] ?? 0.0; + } + + public function toArray(): array + { + return [ + 'report_period' => $this->report_period, + 'collected_in_period' => $this->collected_in_period, + 'pending_in_period' => $this->pending_in_period, + 'refundable_in_period' => $this->refundable_in_period, + 'adjustments_in_period' => $this->adjustments_in_period, + ]; + } +} + +/** + * Individual tax detail object with status tracking + */ +class TaxDetail +{ + public string $invoice_id; + public string $tax_type; // e.g., "state_tax", "city_tax", "county_tax" + public float $tax_rate; + public float $taxable_amount; + public float $tax_amount; + public float $tax_amount_paid; // Amount actually paid + public float $tax_amount_remaining; // Amount still pending + public string $tax_status; // "collected", "pending", "refundable", "partially_paid" + public string $collection_date; // When tax was collected + public string $due_date; // When tax is due + public ?array $payment_history; // Array of PaymentHistory objects + public ?array $metadata; // Additional tax-specific data + + public function __construct(array $attributes = []) + { + $this->invoice_id = $attributes['invoice_id'] ?? ''; + $this->tax_type = $attributes['tax_type'] ?? ''; + $this->tax_rate = $attributes['tax_rate'] ?? 0.0; + $this->taxable_amount = $attributes['taxable_amount'] ?? 0.0; + $this->tax_amount = $attributes['tax_amount'] ?? 0.0; + $this->tax_amount_paid = $attributes['tax_amount_paid'] ?? 0.0; + $this->tax_amount_remaining = $attributes['tax_amount_remaining'] ?? 0.0; + $this->tax_status = $attributes['tax_status'] ?? 'pending'; + $this->collection_date = $attributes['collection_date'] ?? ''; + $this->due_date = $attributes['due_date'] ?? ''; + $this->payment_history = isset($attributes['payment_history']) + ? array_map(fn($payment) => new PaymentHistory($payment), $attributes['payment_history']) + : null; + $this->metadata = $attributes['metadata'] ?? null; + } + + public function toArray(): array + { + return [ + 'invoice_id' => $this->invoice_id, + 'tax_type' => $this->tax_type, + 'tax_rate' => $this->tax_rate, + 'taxable_amount' => $this->taxable_amount, + 'tax_amount' => $this->tax_amount, + 'tax_amount_paid' => $this->tax_amount_paid, + 'tax_amount_remaining' => $this->tax_amount_remaining, + 'tax_status' => $this->tax_status, + 'collection_date' => $this->collection_date, + 'due_date' => $this->due_date, + 'payment_history' => $this->payment_history ? array_map(fn($payment) => $payment->toArray(), $this->payment_history) : null, + 'metadata' => $this->metadata, + ]; + } +} + +/** + * Payment history for tracking partial payments across periods + */ +class PaymentHistory +{ + public string $payment_id; + public string $payment_date; + public string $report_period; // Which period this payment belongs to + public float $payment_amount; + public float $tax_amount_paid; // Tax portion of this payment + public string $payment_method; + public string $status; // "processed", "pending", "failed" + public ?array $allocation_details; // How the payment was allocated + + public function __construct(array $attributes = []) + { + $this->payment_id = $attributes['payment_id'] ?? ''; + $this->payment_date = $attributes['payment_date'] ?? ''; + $this->report_period = $attributes['report_period'] ?? ''; + $this->payment_amount = $attributes['payment_amount'] ?? 0.0; + $this->tax_amount_paid = $attributes['tax_amount_paid'] ?? 0.0; + $this->payment_method = $attributes['payment_method'] ?? ''; + $this->status = $attributes['status'] ?? 'processed'; + $this->allocation_details = $attributes['allocation_details'] ?? null; + } + + public function toArray(): array + { + return [ + 'payment_id' => $this->payment_id, + 'payment_date' => $this->payment_date, + 'report_period' => $this->report_period, + 'payment_amount' => $this->payment_amount, + 'tax_amount_paid' => $this->tax_amount_paid, + 'payment_method' => $this->payment_method, + 'status' => $this->status, + 'allocation_details' => $this->allocation_details, + ]; + } +} + +/** + * Tax adjustment for status changes and corrections + */ +class TaxAdjustment +{ + public string $adjustment_id; + public string $original_invoice_id; + public string $adjustment_type; // "refund", "correction", "status_change" + public string $adjustment_reason; // "invoice_cancelled", "tax_rate_change", "exemption_applied" + public float $adjustment_amount; + public string $adjustment_date; + public string $status; // "pending", "approved", "processed" + public ?array $supporting_documents; // References to supporting docs + + public function __construct(array $attributes = []) + { + $this->adjustment_id = $attributes['adjustment_id'] ?? ''; + $this->original_invoice_id = $attributes['original_invoice_id'] ?? ''; + $this->adjustment_type = $attributes['adjustment_type'] ?? ''; + $this->adjustment_reason = $attributes['adjustment_reason'] ?? ''; + $this->adjustment_amount = $attributes['adjustment_amount'] ?? 0.0; + $this->adjustment_date = $attributes['adjustment_date'] ?? ''; + $this->status = $attributes['status'] ?? 'pending'; + $this->supporting_documents = $attributes['supporting_documents'] ?? null; + } + + public function toArray(): array + { + return [ + 'adjustment_id' => $this->adjustment_id, + 'original_invoice_id' => $this->original_invoice_id, + 'adjustment_type' => $this->adjustment_type, + 'adjustment_reason' => $this->adjustment_reason, + 'adjustment_amount' => $this->adjustment_amount, + 'adjustment_date' => $this->adjustment_date, + 'status' => $this->status, + 'supporting_documents' => $this->supporting_documents, + ]; + } +} diff --git a/app/Services/Report/XLS/TaxReport.php b/app/Services/Report/XLS/TaxReport.php index 233d4cf8f4..4c46041b14 100644 --- a/app/Services/Report/XLS/TaxReport.php +++ b/app/Services/Report/XLS/TaxReport.php @@ -251,11 +251,11 @@ class TaxReport $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($this->spreadsheet); $writer->save($tempFile); - $writer->save('/home/david/ttx.xslx'); + // $writer->save('/home/david/ttx.xslx'); // Read file content $fileContent = file_get_contents($tempFile); - nlog($tempFile); + // nlog($tempFile); // Clean up temp file // unlink($tempFile); diff --git a/tests/Feature/InvoiceTaxReportTest.php b/tests/Feature/InvoiceTaxReportTest.php new file mode 100644 index 0000000000..deb4fe73be --- /dev/null +++ b/tests/Feature/InvoiceTaxReportTest.php @@ -0,0 +1,98 @@ +faker = \Faker\Factory::create(); + + $this->makeTestData(); + } + + public function test_tax_report_meta() + { + $client = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'name' => 'Test Client', + 'address1' => '123 Main St', + 'city' => 'Anytown', + 'state' => 'CA', + 'country_id' => 840, + 'postal_code' => '90210', + ]); + + $client->save(); + + $i = Invoice::factory()->create([ + 'client_id' => $client->id, + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'amount' => 0, + 'balance' => 0, + 'status_id' => 2, + 'total_taxes' => 1, + 'date' => now()->format('Y-m-d'), + 'terms' => 'nada', + 'discount' => 0, + 'tax_rate1' => 10, + 'tax_rate2' => 17.5, + 'tax_rate3' => 5, + 'tax_name1' => 'GST', + 'tax_name2' => 'VAT', + 'tax_name3' => 'CA Sales Tax', + 'uses_inclusive_taxes' => false, + 'line_items' => $this->buildLineItems(), + ]); + + $i = $i->calc()->getInvoice(); + + $this->assertNotNull($i); + + //test tax data object to see if we are using automated taxes. + if(isset($i->tax_data->geoState)){ + $nexus = $i->tax_data->geoState; + $country_nexus = 'USA'; + } + else { + $nexus = strlen($i->client->state ?? '') > 0 ? $i->client->state : $i->company->settings->state; + $country_nexus = strlen($i->client->state ?? '') > 0 ? $i->client->country->iso_3166_2 : $i->company->country()->iso_3166_2; + } + + } +}