1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

first draft

This commit is contained in:
paulwer 2023-11-30 16:00:50 +01:00
parent e1fbbe4268
commit 8a8c3b85c3
11 changed files with 2618 additions and 1377 deletions

View File

@ -0,0 +1,41 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class NordigenApiException extends Exception
{
/**
* Report the exception.
*
* @return void
*/
public function report()
{
//
}
/**
* Render the exception into an HTTP response.
*
* @param Request $request
* @return Response
*/
public function render($request)
{
// $msg = 'Unable to refund the transaction';
$msg = ctrans('texts.error');
if ($this->getMessage() && strlen($this->getMessage()) >= 1) {
$msg = $this->getMessage();
}
return response()->json([
'message' => $msg,
], 400);
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee;
use App\Exceptions\NordigenApiException;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
use App\Helpers\Bank\Nordigen\Transformer\IncomeTransformer;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
// Generate new access token. Token is valid for 24 hours
// Token is automatically injected into every response
$token = $client->createAccessToken();
// Get access token
$accessToken = $client->getAccessToken();
// Get refresh token
$refreshToken = $client->getRefreshToken();
// Exchange refresh token for new access token
$newToken = $client->refreshAccessToken($refreshToken);
// Get list of institutions by country. Country should be in ISO 3166 standard.
$institutions = $client->institution->getInstitutionsByCountry("LV");
// Institution id can be gathered from getInstitutions response.
// Example Revolut ID
$institutionId = "REVOLUT_REVOGB21";
$redirectUri = "https://nordigen.com";
// Initialize new bank connection session
$session = $client->initSession($institutionId, $redirectUri);
// Get link to authorize in the bank
// Authorize with your bank via this link, to gain access to account data
$link = $session["link"];
// requisition id is needed to get accountId in the next step
$requisitionId = $session["requisition_id"];
class Nordigen
{
public bool $test_mode = false;
protected \Nordigen\NordigenPHP\API\NordigenClient $client;
protected string $secret_id;
protected string $secret_key;
public function __construct()
{
$this->secret_id = config('ninja.nordigen.secret_id');
$this->secret_key = config('ninja.nordigen.secret_key');
$this->client = new \Nordigen\NordigenPHP\API\NordigenClient($this->secret_id, $this->secret_key);
}
public function getInstitutions()
{
return $this->client->institution->getInstitutions();
}
public function getValidAccounts()
{
// get all valid requisitions
$requisitions = $this->client->requisition->getRequisitions();
// fetch all valid accounts for activated requisitions
$accounts = [];
foreach ($requisitions as $requisition) {
foreach ($requisition->accounts as $account) {
$account = $account = $this->client->account($account);
array_push($accounts, $account);
}
}
return $accounts;
}
public function cleanup()
{
$requisitions = $this->client->requisition->getRequisitions();
// TODO: filter to older than 2 days created AND (no accounts or invalid)
foreach ($requisitions as $requisition) {
$this->client->requisition->deleteRequisition($requisition->id);
}
}
// account-section: these methods should be used to get data of connected accounts
public function getAccountMetaData(string $account_id)
{
return $this->client->account($account_id)->getAccountMetaData();
}
public function getAccountDetails(string $account_id)
{
return $this->client->account($account_id)->getAccountDetails();
}
public function getAccountBalances(string $account_id)
{
return $this->client->account($account_id)->getAccountBalances();
}
public function getAccountTransactions(string $account_id)
{
return $this->client->account($account_id)->getAccountTransactions();
}
}

View File

@ -0,0 +1,104 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
use App\Helpers\Bank\AccountTransformerInterface;
/**
[0] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => My CD - 8878
[accountStatus] => ACTIVE
[accountNumber] => xxxx8878
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[id] => 12331861
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[accountType] => CD
[displayedName] => LORETTA
[createdDate] => 2022-07-28T06:55:33Z
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
)
)
)
*/
class AccountTransformer implements AccountTransformerInterface
{
public function transform($yodlee_account)
{
$data = [];
if(!property_exists($yodlee_account, 'account'))
return $data;
foreach($yodlee_account->account as $account)
{
$data[] = $this->transformAccount($account);
}
return $data;
}
public function transformAccount($account)
{
return [
'id' => $account->id,
'account_type' => $account->CONTAINER,
// 'account_name' => $account->accountName,
'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname,
'account_status' => $account->accountStatus,
'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '',
'provider_account_id' => $account->providerAccountId,
'provider_id' => $account->providerId,
'provider_name' => $account->providerName,
'nickname' => property_exists($account, 'nickname') ? $account->nickname : '',
'current_balance' => property_exists($account, 'currentBalance') ? $account->currentBalance->amount : 0,
'account_currency' => property_exists($account, 'currency') ? $account->currentBalance->currency : '',
];
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
/**
"date": "string",
"sourceId": "string",
"symbol": "string",
"cusipNumber": "string",
"highLevelCategoryId": 0,
"detailCategoryId": 0,
"description": {},
"memo": "string",
"settleDate": "string",
"type": "string",
"intermediary": [],
"baseType": "CREDIT",
"categorySource": "SYSTEM",
"principal": {},
"lastUpdated": "string",
"interest": {},
"price": {},
"commission": {},
"id": 0,
"merchantType": "string",
"amount": {
"amount": 0,
"convertedAmount": 0,
"currency": "USD",
"convertedCurrency": "USD"
},
"checkNumber": "string",
"isPhysical": true,
"quantity": 0,
"valoren": "string",
"isManual": true,
"merchant": {
"website": "string",
"address": {},
"contact": {},
"categoryLabel": [],
"coordinates": {},
"name": "string",
"id": "string",
"source": "YODLEE",
"logoURL": "string"
},
"sedol": "string",
"transactionDate": "string",
"categoryType": "TRANSFER",
"accountId": 0,
"createdDate": "string",
"sourceType": "AGGREGATED",
"CONTAINER": "bank",
"postDate": "string",
"parentCategoryId": 0,
"subType": "OVERDRAFT_CHARGE",
"category": "string",
"runningBalance": {},
"categoryId": 0,
"holdingDescription": "string",
"isin": "string",
"status": "POSTED"
*/
class ExpenseTransformer
{
}

View File

@ -0,0 +1,187 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
use App\Helpers\Bank\BankRevenueInterface;
use App\Utils\Traits\AppSetup;
use Illuminate\Support\Facades\Cache;
/**
"date": "string",
"sourceId": "string",
"symbol": "string",
"cusipNumber": "string",
"highLevelCategoryId": 0,
"detailCategoryId": 0,
"description": {},
"memo": "string",
"settleDate": "string",
"type": "string",
"intermediary": [],
"baseType": "CREDIT",
"categorySource": "SYSTEM",
"principal": {},
"lastUpdated": "string",
"interest": {},
"price": {},
"commission": {},
"id": 0,
"merchantType": "string",
"amount": {
"amount": 0,
"convertedAmount": 0,
"currency": "USD",
"convertedCurrency": "USD"
},
"checkNumber": "string",
"isPhysical": true,
"quantity": 0,
"valoren": "string",
"isManual": true,
"merchant": {
"website": "string",
"address": {},
"contact": {},
"categoryLabel": [],
"coordinates": {},
"name": "string",
"id": "string",
"source": "YODLEE",
"logoURL": "string"
},
"sedol": "string",
"transactionDate": "string",
"categoryType": "TRANSFER",
"accountId": 0,
"createdDate": "string",
"sourceType": "AGGREGATED",
"CONTAINER": "bank",
"postDate": "string",
"parentCategoryId": 0,
"subType": "OVERDRAFT_CHARGE",
"category": "string",
"runningBalance": {},
"categoryId": 0,
"holdingDescription": "string",
"isin": "string",
"status": "POSTED"
(
[CONTAINER] => bank
[id] => 103953585
[amount] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
[categoryType] => UNCATEGORIZE
[categoryId] => 1
[category] => Uncategorized
[categorySource] => SYSTEM
[highLevelCategoryId] => 10000017
[createdDate] => 2022-08-04T21:50:17Z
[lastUpdated] => 2022-08-04T21:50:17Z
[description] => stdClass Object
(
[original] => CHEROKEE NATION TAX TA TAHLEQUAH OK
)
[isManual] =>
[sourceType] => AGGREGATED
[date] => 2022-08-03
[transactionDate] => 2022-08-03
[postDate] => 2022-08-03
[status] => POSTED
[accountId] => 12331794
[runningBalance] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
[checkNumber] => 998
)
*/
class IncomeTransformer implements BankRevenueInterface
{
use AppSetup;
public function transform($transaction)
{
$data = [];
if(!property_exists($transaction, 'transaction'))
return $data;
foreach($transaction->transaction as $transaction)
{
$data[] = $this->transformTransaction($transaction);
}
return $data;
}
public function transformTransaction($transaction)
{
return [
'transaction_id' => $transaction->id,
'amount' => $transaction->amount->amount,
'currency_id' => $this->convertCurrency($transaction->amount->currency),
'account_type' => $transaction->CONTAINER,
'category_id' => $transaction->highLevelCategoryId,
'category_type' => $transaction->categoryType,
'date' => $transaction->date,
'bank_account_id' => $transaction->accountId,
'description' => $transaction->description->original,
'base_type' => property_exists($transaction, 'baseType') ? $transaction->baseType : $this->calculateBaseType($transaction),
];
}
private function calculateBaseType($transaction)
{
//CREDIT / DEBIT
if(property_exists($transaction, 'highLevelCategoryId') && $transaction->highLevelCategoryId == 10000012)
return 'CREDIT';
return 'DEBIT';
}
private function convertCurrency(string $code)
{
$currencies = Cache::get('currencies');
if (! $currencies) {
$this->buildCache(true);
}
$currency = $currencies->filter(function ($item) use($code){
return $item->code == $code;
})->first();
if($currency)
return $currency->id;
return 1;
}
}

View File

@ -0,0 +1,301 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Bank;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Yodlee\YodleeAuthRequest;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Models\BankIntegration;
use Illuminate\Http\Request;
class YodleeController extends BaseController
{
public function auth(YodleeAuthRequest $request)
{
// create a user at this point
// use the one time token here to pull in the actual user
// store the user_account_id on the accounts table
$nordigen = new Nordigen();
$company = $request->getCompany();
//ensure user is enterprise!!
if ($company->account->bank_integration_account_id) {
$flow = 'edit';
$token = $company->account->bank_integration_account_id;
} else {
$flow = 'add';
$response = $yodlee->createUser($company);
$token = $response->user->loginName;
$company->account->bank_integration_account_id = $token;
$company->push();
}
$yodlee = new Yodlee($token);
if ($request->has('window_closed') && $request->input("window_closed") == "true")
$this->getAccounts($company, $token);
$data = [
'access_token' => $yodlee->getAccessToken(),
'fasttrack_url' => $yodlee->getFastTrackUrl(),
'config_name' => config('ninja.yodlee.config_name'),
'flow' => $flow,
'company' => $company,
'account' => $company->account,
'completed' => $request->has('window_closed') ? true : false,
];
return view('bank.yodlee.auth', $data);
}
private function getAccounts($company, $token)
{
$nordigen = new Nordigen($token);
$accounts = $nordigen->getAccounts();
foreach ($accounts as $account) {
if (!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->exists()) {
$bank_integration = new BankIntegration();
$bank_integration->company_id = $company->id;
$bank_integration->account_id = $company->account_id;
$bank_integration->user_id = $company->owner()->id;
$bank_integration->bank_account_id = $account['id'];
$bank_integration->bank_account_type = $account['account_type'];
$bank_integration->bank_account_name = $account['account_name'];
$bank_integration->bank_account_status = $account['account_status'];
$bank_integration->bank_account_number = $account['account_number'];
$bank_integration->provider_id = $account['provider_id'];
$bank_integration->provider_name = $account['provider_name'];
$bank_integration->nickname = $account['nickname'];
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->from_date = now()->subYear();
$bank_integration->save();
}
}
$company->account->bank_integrations->each(function ($bank_integration) use ($company) {
ProcessBankTransactions::dispatch($company->account->bank_integration_account_id, $bank_integration);
});
}
/**
* Process Yodlee Refresh Webhook.
*
*
* @OA\Post(
* path="/api/v1/yodlee/refresh",
* operationId="yodleeRefreshWebhook",
* tags={"yodlee"},
* summary="Processing webhooks from Yodlee",
* description="Notifies the system when a data point can be refreshed",
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
/*
{
"event":{
"info":"REFRESH.PROCESS_COMPLETED",
"loginName":"fri21",
"data":{
"providerAccount":[
{
"id":10995860,
"providerId":16441,
"isManual":false,
"createdDate":"2017-12-22T05:47:35Z",
"aggregationSource":"USER",
"status":"SUCCESS",
"requestId":"NSyMGo+R4dktywIu3hBIkc3PgWA=",
"dataset":[
{
"name":"BASIC_AGG_DATA",
"additionalStatus":"AVAILABLE_DATA_RETRIEVED",
"updateEligibility":"ALLOW_UPDATE",
"lastUpdated":"2017-12-22T05:48:16Z",
"lastUpdateAttempt":"2017-12-22T05:48:16Z"
}
]
}
]
}
}
}*/
public function refreshWebhook(Request $request)
{
//we should ignore this one
nlog("yodlee refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"notificationId":"63c73475-4db5-49ef-8553-8303337ca7c3",
"info":"LATEST_BALANCE_UPDATES",
"loginName":"user1",
"data":{
"providerAccountId":658552,
"latestBalanceEvent":[
{
"accountId":12345,
"status":"SUCCESS"
},
{
"accountId":12346,
"status":"FAILED"
}
]
}
}
}
*/
public function balanceWebhook(Request $request)
{
nlog("yodlee refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"data":[
{
"autoRefresh":{
"additionalStatus":"SCHEDULED",
"status":"ENABLED"
},
"accountIds":[
1112645899,
1112645898
],
"loginName":"YSL1555332811628",
"providerAccountId":11381459
}
],
"notificationTime":"2019-06-14T04:49:39Z",
"notificationId":"4e672150-156048777",
"info":"AUTO_REFRESH_UPDATES"
}
}
*/
public function refreshUpdatesWebhook(Request $request)
{
//notifies a user if there are problems with yodlee accessing the data
nlog("update refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
"event": {
"notificationId": "64b7ed1a-1530523285",
"info": "DATA_UPDATES.USER_DATA",
"data": {
"userCount": 1,
"fromDate": "2017-11-10T10:18:44Z",
"toDate": "2017-11-10T11:18:43Z",
"userData": [{
"user": {
"loginName": "YSL1484052178554"
},
"links": [{
"methodType": "GET",
"rel": "getUserData",
"href": "dataExtracts/userData?fromDate=2017-11-10T10:18:44Z&toDate=2017-11-10T11:18:43Z&loginName=YSL1484052178554"
}]
}]
}
}
*/
public function dataUpdatesWebhook(Request $request)
{
//this is the main hook we use for notifications
nlog("data refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
}

View File

@ -20,8 +20,9 @@ class BankIntegration extends BaseModel
use SoftDeletes;
use Filterable;
use Excludable;
protected $fillable = [
'integration_type',
'bank_account_name',
'provider_name',
'bank_account_number',
@ -36,6 +37,12 @@ class BankIntegration extends BaseModel
protected $dates = [
];
const INTEGRATION_TYPE_NONE = null;
const INTEGRATION_TYPE_YODLEE = 'YODLEE';
const INTEGRATION_TYPE_NORDIGEN = 'NORDIGEN';
public function getEntityType()
{
return self::class;
@ -61,4 +68,4 @@ class BankIntegration extends BaseModel
return $this->hasMany(BankTransaction::class)->withTrashed();
}
}
}

View File

@ -70,11 +70,11 @@
"microsoft/microsoft-graph": "^1.69",
"mollie/mollie-api-php": "^2.36",
"nelexa/zip": "^4.0",
"nordigen/nordigen-php": "^1.1",
"nwidart/laravel-modules": "8.3",
"omnipay/paypal": "^3.0",
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0",
"turbo124/predis": "^1.1",
"razorpay/razorpay": "2.*",
"sentry/sentry-laravel": "^3",
"setasign/fpdf": "^1.8",
@ -89,6 +89,7 @@
"symfony/postmark-mailer": "^6.1",
"tijsverkoyen/css-to-inline-styles": "^2.2",
"turbo124/beacon": "^1.3",
"turbo124/predis": "^1.1",
"twilio/sdk": "^6.40",
"webpatser/laravel-countries": "dev-master#75992ad",
"wepay/php-sdk": "^0.3"
@ -157,7 +158,10 @@
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
"optimize-autoloader": true,
"allow-plugins": {
"php-http/discovery": true
}
},
"minimum-stability": "dev",
"prefer-stable": true

3005
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
<?php
use App\Models\BankIntegration;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('bank_integration', function (Blueprint $table) {
$table->string('integration_type')->nullable();
});
// migrate old account to be used with yodlee
BankIntegration::query()->whereNull('integration_type')->cursor()->each(function ($bank_integration) {
$bank_integration->integration_type = BankIntegration::INTEGRATION_TYPE_YODLEE;
$bank_integration->save();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('bank_integration', function (Blueprint $table) {
$table->dropColumn('integration_id');
});
}
};

View File

@ -1,49 +1,49 @@
{
"/js/app.js": "/js/app.js?id=7b6124b74168ccb1cc7da22f7a2bc9ed",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=b6723e0b8ea33f1f50617fa5f289a9d3",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=faf4828cc6b3b73b69c53d3046661884",
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js?id=f42dd0caddb3603e71db061924c4b172",
"/js/clients/payments/forte-ach-payment.js": "/js/clients/payments/forte-ach-payment.js?id=b8173c7c0dee76bf9ae6312a963ae0e4",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=207f218c44553470287f35f33a7eb154",
"/js/clients/payments/stripe-klarna.js": "/js/clients/payments/stripe-klarna.js?id=7268f9282c6bb3b04d19d11a7b0c1681",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=404b7ee18e420de0e73f5402b7e39122",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=2f0c4e3bab30a98e33ac768255113174",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=9bb483a89a887f753e49c0b635d6276a",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=752e2bb6390f1a422e31868cf2a2bf67",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=4fc5dec1bc4fc21b9e32b1b490c3e7ae",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=018ecad3a1bcc1ecc47f76754a573ff2",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=7cb96275b3eb4901054564c654fb60e3",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=3a4c5cfac7dd4c9218be55945c3c8e85",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=1e58e219878ce3f3ee4d313346ad5f68",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=6e7c8ab039a239727317ae8622de10db",
"/js/setup/setup.js": "/js/setup/setup.js?id=8cab3339ef48418e1fb2e7a9259d51ca",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=cf50b5ba1fcd1d184bf0c10d710672c8",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=682de6347049b32c9488f39c78a68ace",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=d3c404bb646f1aeaf2382a8c57ab8e1a",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=e1c0599d6f7dc163b549a6df0b3490b4",
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js?id=8b036822abaa4ceb379008fc14208dc2",
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js?id=de0b1d0c6da7ff509bef3aee8d09e7f8",
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js?id=92ef8632637d335cd0e4bc29a05b7df8",
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js?id=af85b3f6d53c55b5d0e3a80ef58ce0de",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=3869bc6d80acc83f81d9afe8efaae728",
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=7cd5a1d95d33ada211ce185ad6e4bb33",
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js?id=27274d334aed0824ce4654fa22132f7f",
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js?id=f85ebb6a77002afd350086d1274b6af5",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=238e7001420a22b001856193689a1e70",
"/js/clients/statements/view.js": "/js/clients/statements/view.js?id=13e043123f1e58409394458a70461d63",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js?id=494f58d2fd8984792833ba7d3055de08",
"/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js?id=77d4e397d193196e482af80737bff64a",
"/js/clients/payment_methods/authorize-checkout-card.js": "/js/clients/payment_methods/authorize-checkout-card.js?id=659c4287fb8ef1c458071c206c4d965d",
"/js/clients/payments/stripe-giropay.js": "/js/clients/payments/stripe-giropay.js?id=852a9abf5f3a29f5d7d2f989cbeab374",
"/js/clients/payments/stripe-acss.js": "/js/clients/payments/stripe-acss.js?id=447c587a5eeb0c1de3091c8358db7ad7",
"/js/clients/payments/stripe-bancontact.js": "/js/clients/payments/stripe-bancontact.js?id=f694d3f9f01e4550cb5a3eb6cb43c12d",
"/js/clients/payments/stripe-becs.js": "/js/clients/payments/stripe-becs.js?id=97ea3555a8504662eda5fce9c9115e5a",
"/js/clients/payments/stripe-eps.js": "/js/clients/payments/stripe-eps.js?id=749cba1332a29baa444b37cee2ade2d7",
"/js/clients/payments/stripe-ideal.js": "/js/clients/payments/stripe-ideal.js?id=34cf4ee3f189427fb69d0df8f5a4b766",
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=448b197a1d94b4408e130b5b8b1c2e53",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=7015e43eb5f9f9f2f45f54b41b5780a0",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js?id=243c2929386b10c6a0c49ca3bcabfb2d",
"/css/app.css": "/css/app.css?id=0cb847167b91d8db2ca50d30e0d691ae",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ada60afcedcb7c",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js?id=15f52a1ee547f2bdd46e56747332ca2d"
"/js/app.js": "/js/app.js",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js",
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js",
"/js/clients/payments/forte-ach-payment.js": "/js/clients/payments/forte-ach-payment.js",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js",
"/js/clients/payments/stripe-klarna.js": "/js/clients/payments/stripe-klarna.js",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js",
"/js/setup/setup.js": "/js/setup/setup.js",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js",
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js",
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js",
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js",
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js",
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js",
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js",
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js",
"/js/clients/statements/view.js": "/js/clients/statements/view.js",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js",
"/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js",
"/js/clients/payment_methods/authorize-checkout-card.js": "/js/clients/payment_methods/authorize-checkout-card.js",
"/js/clients/payments/stripe-giropay.js": "/js/clients/payments/stripe-giropay.js",
"/js/clients/payments/stripe-acss.js": "/js/clients/payments/stripe-acss.js",
"/js/clients/payments/stripe-bancontact.js": "/js/clients/payments/stripe-bancontact.js",
"/js/clients/payments/stripe-becs.js": "/js/clients/payments/stripe-becs.js",
"/js/clients/payments/stripe-eps.js": "/js/clients/payments/stripe-eps.js",
"/js/clients/payments/stripe-ideal.js": "/js/clients/payments/stripe-ideal.js",
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js",
"/css/app.css": "/css/app.css",
"/css/card-js.min.css": "/css/card-js.min.css",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js"
}