mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 02:57:01 +00:00
125
app/DataMapper/IncomeAccountMap.php
Normal file
125
app/DataMapper/IncomeAccountMap.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Models\Product;
|
||||
|
||||
/**
|
||||
* Holds the QuickBooks income account ID per product type.
|
||||
*
|
||||
* Child properties map Product::PRODUCT_TYPE_* to QuickBooks account ID (string|null).
|
||||
* Initialized with no values; call setAccountId() or assign properties to populate.
|
||||
*/
|
||||
class IncomeAccountMap
|
||||
{
|
||||
public ?string $physical = null;
|
||||
|
||||
public ?string $service = null;
|
||||
|
||||
public ?string $digital = null;
|
||||
|
||||
public ?string $shipping = null;
|
||||
|
||||
public ?string $exempt = null;
|
||||
|
||||
public ?string $reduced_tax = null;
|
||||
|
||||
public ?string $override_tax = null;
|
||||
|
||||
public ?string $zero_rated = null;
|
||||
|
||||
public ?string $reverse_tax = null;
|
||||
|
||||
public ?string $intra_community = null;
|
||||
|
||||
/**
|
||||
* Initialize from attributes array.
|
||||
* Accepts array with int keys (Product::PRODUCT_TYPE_*) or string keys (property names).
|
||||
*/
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->physical = $attributes['physical'] ?? null;
|
||||
$this->service = $attributes['service'] ?? null;
|
||||
$this->digital = $attributes['digital'] ?? null;
|
||||
$this->shipping = $attributes['shipping'] ?? null;
|
||||
$this->exempt = $attributes['exempt'] ?? null;
|
||||
$this->reduced_tax = $attributes['reduced_tax'] ?? null;
|
||||
$this->override_tax = $attributes['override_tax'] ?? null;
|
||||
$this->zero_rated = $attributes['zero_rated'] ?? null;
|
||||
$this->reverse_tax = $attributes['reverse_tax'] ?? null;
|
||||
$this->intra_community = $attributes['intra_community'] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getAccountId
|
||||
*
|
||||
* Gets the Quickbooks Income Account ID for a given product tax_id.
|
||||
* @param string $product_tax_id
|
||||
* @return string|null
|
||||
*/
|
||||
public function getAccountId(?string $product_tax_id): ?string
|
||||
{
|
||||
/**
|
||||
* @var string|null $prop
|
||||
*
|
||||
* Translates "2" => "service"
|
||||
*
|
||||
* */
|
||||
$prop = $this->getPropertyName($product_tax_id);
|
||||
|
||||
return $prop ? $this->{$prop} : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPropertyName
|
||||
*
|
||||
* Tranlates the $item->tax_id => property name.
|
||||
*
|
||||
* Gets the property name for a given product tax_id.
|
||||
* @param int|string $key
|
||||
* @return string|null
|
||||
*/
|
||||
private function getPropertyName(int|string $key): ?string
|
||||
{
|
||||
return match ((string)$key) {
|
||||
(string)Product::PRODUCT_TYPE_PHYSICAL => 'physical',
|
||||
(string)Product::PRODUCT_TYPE_SERVICE => 'service',
|
||||
(string)Product::PRODUCT_TYPE_DIGITAL => 'digital',
|
||||
(string)Product::PRODUCT_TYPE_SHIPPING => 'shipping',
|
||||
(string)Product::PRODUCT_TYPE_EXEMPT => 'exempt',
|
||||
(string)Product::PRODUCT_TYPE_REDUCED_TAX => 'reduced_tax',
|
||||
(string)Product::PRODUCT_TYPE_OVERRIDE_TAX => 'override_tax',
|
||||
(string)Product::PRODUCT_TYPE_ZERO_RATED => 'zero_rated',
|
||||
(string)Product::PRODUCT_TYPE_REVERSE_TAX => 'reverse_tax',
|
||||
(string)Product::PRODUCT_INTRA_COMMUNITY => 'intra_community',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'physical' => $this->physical,
|
||||
'service' => $this->service,
|
||||
'digital' => $this->digital,
|
||||
'shipping' => $this->shipping,
|
||||
'exempt' => $this->exempt,
|
||||
'reduced_tax' => $this->reduced_tax,
|
||||
'override_tax' => $this->override_tax,
|
||||
'zero_rated' => $this->zero_rated,
|
||||
'reverse_tax' => $this->reverse_tax,
|
||||
'intra_community' => $this->intra_community,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,15 @@
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Models\Product;
|
||||
|
||||
/**
|
||||
* QuickbooksSync.
|
||||
*
|
||||
* Product type to income account mapping:
|
||||
* Keys are Product::PRODUCT_TYPE_* constants (int). Values are QuickBooks account IDs (string|null).
|
||||
* Example: [Product::PRODUCT_TYPE_SERVICE => '123', Product::PRODUCT_TYPE_PHYSICAL => '456']
|
||||
* Null values indicate the account has not been configured for that product type.
|
||||
*/
|
||||
class QuickbooksSync
|
||||
{
|
||||
@@ -35,9 +42,11 @@ class QuickbooksSync
|
||||
|
||||
public QuickbooksSyncMap $expense;
|
||||
|
||||
public string $default_income_account = '';
|
||||
|
||||
public string $default_expense_account = '';
|
||||
/**
|
||||
* QuickBooks income account ID per product type.
|
||||
* Use getAccountId(int $productTypeId) or the typed properties (physical, service, etc.).
|
||||
*/
|
||||
public IncomeAccountMap $income_account_map;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
@@ -50,8 +59,8 @@ class QuickbooksSync
|
||||
$this->product = new QuickbooksSyncMap($attributes['product'] ?? []);
|
||||
$this->payment = new QuickbooksSyncMap($attributes['payment'] ?? []);
|
||||
$this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []);
|
||||
$this->default_income_account = $attributes['default_income_account'] ?? '';
|
||||
$this->default_expense_account = $attributes['default_expense_account'] ?? '';
|
||||
$this->income_account_map = new IncomeAccountMap($attributes['income_account_map'] ?? []);
|
||||
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
@@ -66,8 +75,7 @@ class QuickbooksSync
|
||||
'product' => $this->product->toArray(),
|
||||
'payment' => $this->payment->toArray(),
|
||||
'expense' => $this->expense->toArray(),
|
||||
'default_income_account' => $this->default_income_account,
|
||||
'default_expense_account' => $this->default_expense_account,
|
||||
'income_account_map' => $this->income_account_map->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ class ImportQuickbooksController extends BaseController
|
||||
*
|
||||
* Starts the Quickbooks authorization process.
|
||||
*
|
||||
* @param mixed $request
|
||||
* @param AuthQuickbooksRequest $request
|
||||
* @param string $token
|
||||
* @return RedirectResponse
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
|
||||
{
|
||||
@@ -54,7 +54,7 @@ class ImportQuickbooksController extends BaseController
|
||||
* Handles the callback from Quickbooks after authorization.
|
||||
*
|
||||
* @param AuthorizedQuickbooksRequest $request
|
||||
* @return RedirectResponse
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ class QuickbooksService
|
||||
'ClientSecret' => config('services.quickbooks.client_secret'),
|
||||
'auth_mode' => 'oauth2',
|
||||
'scope' => "com.intuit.quickbooks.accounting",
|
||||
'RedirectURI' => $this->testMode ? 'https://qb.romulus.com.au/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized',
|
||||
'RedirectURI' => config('services.quickbooks.redirect'),
|
||||
'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
|
||||
];
|
||||
|
||||
@@ -169,6 +169,7 @@ class QuickbooksService
|
||||
}
|
||||
|
||||
nlog('Quickbooks token expired and could not be refreshed => ' .$this->company->company_key);
|
||||
|
||||
throw new \Exception('Quickbooks token expired and could not be refreshed');
|
||||
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class InvoiceTransformer extends BaseTransformer
|
||||
}
|
||||
}
|
||||
|
||||
$line_items[] = [
|
||||
$line_payload = [
|
||||
'LineNum' => $line_num,
|
||||
'DetailType' => 'SalesItemLineDetail',
|
||||
'SalesItemLineDetail' => [
|
||||
@@ -80,6 +80,12 @@ class InvoiceTransformer extends BaseTransformer
|
||||
'Amount' => $line_item->line_total ?? ($line_item->cost * ($line_item->quantity ?? 1)),
|
||||
];
|
||||
|
||||
|
||||
//check here if we need to inject the income account reference
|
||||
// $line_payload['AccountRef'] = ['value' => $income_account_qb_id];
|
||||
|
||||
$line_items[] = $line_payload;
|
||||
|
||||
$line_num++;
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ class TemplateEngine
|
||||
$data['title'] = '';
|
||||
$data['body'] = '$body';
|
||||
$data['footer'] = '';
|
||||
$data['logo'] = $user->company()->present()->logo();
|
||||
$data['logo'] = $user->company()->present()->logo($this->settings);
|
||||
|
||||
if ($this->entity_obj->client()->exists()) {
|
||||
$data = array_merge($data, Helpers::sharedEmailVariables($this->entity_obj->client));
|
||||
|
||||
516
composer.lock
generated
516
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -77,9 +77,7 @@ class QuickbooksTest extends TestCase
|
||||
"Taxable" => true,
|
||||
"UnitPrice" => $non_inventory_product->price,
|
||||
"Type" => "NonInventory",
|
||||
"IncomeAccountRef" => [
|
||||
"value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
|
||||
],
|
||||
|
||||
// "AssetAccountRef" => [
|
||||
// "value" => "81", // Replace with your actual asset account ID
|
||||
// "name" => "Inventory Asset"
|
||||
@@ -109,9 +107,7 @@ class QuickbooksTest extends TestCase
|
||||
"Taxable" => true,
|
||||
"UnitPrice" => $service_product->price,
|
||||
"Type" => "Service",
|
||||
"IncomeAccountRef" => [
|
||||
"value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
|
||||
],
|
||||
|
||||
"TrackQtyOnHand" => false,
|
||||
|
||||
]);
|
||||
|
||||
@@ -187,16 +187,13 @@ class QuickbooksSettingsSerializationComparisonTest extends TestCase
|
||||
$this->assertEquals('bidirectional', $oldDecoded['settings']['client']['direction']);
|
||||
$this->assertEquals('bidirectional', $newDecoded['settings']['client']['direction']);
|
||||
|
||||
// Both produce equivalent results, but toArray() is explicit
|
||||
$this->assertEquals(
|
||||
json_encode($oldDecoded),
|
||||
json_encode($newDecoded),
|
||||
'Both methods produce equivalent results, but toArray() is explicit and maintainable'
|
||||
);
|
||||
|
||||
// The key difference: toArray() gives explicit control
|
||||
// toArray() is the canonical form for persistence: explicit control, consistent shape
|
||||
$this->assertIsArray($newArray, 'toArray() explicitly returns an array structure');
|
||||
$this->assertIsString($newArray['settings']['client']['direction'],
|
||||
$this->assertIsString($newArray['settings']['client']['direction'],
|
||||
'toArray() explicitly converts enum to string value');
|
||||
|
||||
// income_account_map uses int keys (Product::PRODUCT_TYPE_*) in toArray() for storage
|
||||
$this->assertArrayHasKey('income_account_map', $newDecoded['settings']);
|
||||
$this->assertIsArray($newDecoded['settings']['income_account_map']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,7 @@ class QuickbooksSettingsSerializationTest extends TestCase
|
||||
'client' => [
|
||||
'direction' => SyncDirection::PULL->value,
|
||||
],
|
||||
'default_income_account' => 'income_account_123',
|
||||
'default_expense_account' => 'expense_account_456',
|
||||
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -119,9 +118,7 @@ class QuickbooksSettingsSerializationTest extends TestCase
|
||||
// Verify nested QuickbooksSync structure
|
||||
$this->assertIsArray($array['settings']);
|
||||
$this->assertArrayHasKey('client', $array['settings']);
|
||||
$this->assertArrayHasKey('default_income_account', $array['settings']);
|
||||
$this->assertEquals('income_account_123', $array['settings']['default_income_account']);
|
||||
$this->assertEquals('expense_account_456', $array['settings']['default_expense_account']);
|
||||
$this->assertArrayHasKey('income_account_map', $array['settings']);
|
||||
|
||||
// Verify nested QuickbooksSyncMap structure
|
||||
$this->assertIsArray($array['settings']['client']);
|
||||
@@ -151,8 +148,6 @@ class QuickbooksSettingsSerializationTest extends TestCase
|
||||
'product' => [
|
||||
'direction' => SyncDirection::BIDIRECTIONAL->value,
|
||||
],
|
||||
'default_income_account' => 'income_123',
|
||||
'default_expense_account' => 'expense_456',
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -184,8 +179,6 @@ class QuickbooksSettingsSerializationTest extends TestCase
|
||||
|
||||
// Verify nested settings are preserved
|
||||
$this->assertInstanceOf(QuickbooksSync::class, $deserialized->settings);
|
||||
$this->assertEquals('income_123', $deserialized->settings->default_income_account);
|
||||
$this->assertEquals('expense_456', $deserialized->settings->default_expense_account);
|
||||
|
||||
// Verify enum values are preserved correctly
|
||||
$this->assertInstanceOf(QuickbooksSyncMap::class, $deserialized->settings->client);
|
||||
|
||||
Reference in New Issue
Block a user