Merge pull request #11612 from turbo124/v5-develop

Dependency updates
This commit is contained in:
David Bomba
2026-01-29 07:50:05 +11:00
committed by GitHub
10 changed files with 421 additions and 295 deletions

View 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,
];
}
}

View File

@@ -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(),
];
}
}

View File

@@ -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)
{

View File

@@ -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');
}

View File

@@ -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++;
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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,
]);

View File

@@ -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']);
}
}

View File

@@ -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);