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

Merge pull request #9535 from turbo124/v5-develop

Updates for BTC payment driver
This commit is contained in:
David Bomba 2024-05-21 11:59:51 +10:00 committed by GitHub
commit 643f8a64c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 1329 additions and 891 deletions

View File

@ -52,6 +52,10 @@ class DbQuery extends GenericMixedMetric
public $string_metric7 = 'ip_address';
public $string_metric8 = 'client_version';
public $string_metric9 = 'platform';
/**
* The counter
* set to 1.

View File

@ -32,7 +32,6 @@ use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\ExpenseCategory;
use League\Fractal\Resource\Item;
use App\DataMapper\EDoc\Schema\RO;
use App\Models\BankTransactionRule;
use Illuminate\Support\Facades\Auth;
use App\Transformers\ArraySerializer;
@ -42,6 +41,7 @@ use Illuminate\Database\Eloquent\Builder;
use League\Fractal\Serializer\JsonApiSerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Illuminate\Contracts\Container\BindingResolutionException;
use Invoiceninja\Einvoice\Decoder\Schema;
/**
* Class BaseController.
@ -890,7 +890,6 @@ class BaseController extends Controller
/** @phpstan-ignore-next-line **/
$query = $paginator->getCollection();// @phpstan-ignore-line
$resource = new Collection($query, $transformer, $this->entity_type);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
}
@ -998,8 +997,8 @@ class BaseController extends Controller
if(request()->has('einvoice')){
$ro = new RO();
$response_data['einvoice_schema'] = $ro();
$ro = new Schema();
$response_data['einvoice_schema'] = $ro('FACT1');
}

View File

@ -73,7 +73,22 @@ class QueryLogging
$ip = $request->ip();
}
LightLogs::create(new DbQuery($request->method(), substr(urldecode($request->url()), 0, 180), $count, $time, $ip))
$client_version = $request->server('HTTP_USER_AGENT');
$platform = '';
if ($request->hasHeader('X-CLIENT-PLATFORM')) {
$platform = $request->header('X-CLIENT-PLATFORM');
}
elseif($request->hasHeader('X-React')){
$platform = 'react';
}
if ($request->hasHeader('X-CLIENT-VERSION'))
{
$client_version = $request->header('X-CLIENT-VERSION');
}
LightLogs::create(new DbQuery($request->method(), substr(urldecode($request->url()), 0, 180), $count, $time, $ip, $client_version, $platform))
->batch();
}

View File

@ -17,6 +17,7 @@ use App\Http\ValidationRules\Company\ValidSubdomain;
use App\Http\ValidationRules\ValidSettingsRule;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronica;
class UpdateCompanyRequest extends Request
{
@ -113,6 +114,11 @@ class UpdateCompanyRequest extends Request
$input['smtp_verify_peer'] == 'true' ? true : false;
}
// if(isset($input['e_invoice'])){
// nlog("am i set?");
// $r = FatturaElettronica::validate($input['e_invoice']);
// }
$this->replace($input);
}

View File

@ -241,6 +241,32 @@ class CompanyImport implements ShouldQueue
CompanyGateway::class => [
'always_show_required_fields',
]
],
'5.8.57' => [
Company::class => [
'einvoice',
'e_invoice',
],
Invoice::class => [
'einvoice',
'e_invoice',
],
Quote::class => [
'einvoice',
'e_invoice',
],
Credit::class => [
'einvoice',
'e_invoice',
],
PurchaseOrder::class => [
'einvoice',
'e_invoice',
],
Expense::class => [
'einvoice',
'e_invoice',
],
]
];

View File

@ -136,8 +136,8 @@ class NinjaMailerJob implements ShouldQueue
$mailable = $this->nmo->mailable;
/** May need to re-build it here */
if (Ninja::isHosted() && method_exists($mailable, 'build')) {
/** May need to re-build it here @todo explain why we need this? */
if (Ninja::isHosted() && method_exists($mailable, 'build') && $this->nmo->settings->email_style != "custom") {
$mailable->build();
}

View File

@ -116,13 +116,17 @@ class EmailPayment implements ShouldQueue
$invoice->invitations->each(function ($invite) use ($email_builder) {
$cloned_mailable = unserialize(serialize($email_builder));
$nmo = new NinjaMailerObject();
$nmo->mailable = new TemplateEmail($email_builder, $invite->contact, $invite);
$nmo->mailable = new TemplateEmail($cloned_mailable, $invite->contact, $invite);
$nmo->to_user = $invite->contact;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
$nmo->entity = $this->payment;
(new NinjaMailerJob($nmo))->handle();
$nmo = null;
event(new PaymentWasEmailed($this->payment, $this->payment->company, $invite->contact, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));

View File

@ -126,7 +126,7 @@ class ReminderJob implements ShouldQueue
}
$reminder_template = $invoice->calculateTemplate('invoice');
nrlog("reminder template = {$reminder_template}");
nrlog("#{$invoice->number} => reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$fees = $this->calcLateFee($invoice, $reminder_template);

View File

@ -134,10 +134,6 @@ class InvoiceEmailEngine extends BaseEmailEngine
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $this->invoice->numberFormatter().'.pdf']]);
}
// $hash = Str::uuid();
// $url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
// Cache::put($hash, $url, now()->addHour());
//attach third party documents
if ($this->client->getSetting('document_email_attachment') !== false && $this->invoice->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
if ($this->invoice->recurring_invoice()->exists()) {

View File

@ -99,7 +99,6 @@ class PaymentEmailEngine extends BaseEmailEngine
->setViewLink('')
->setViewText('');
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$template_in_use = false;

View File

@ -31,6 +31,7 @@ use Laracasts\Presenter\PresentableTrait;
* App\Models\Account
*
* @property int $id
* @property int $email_quota
* @property string|null $plan
* @property string|null $plan_term
* @property string|null $plan_started

View File

@ -364,6 +364,7 @@ class Company extends BaseModel
'smtp_encryption',
'smtp_local_domain',
'smtp_verify_peer',
'e_invoice',
];
protected $hidden = [
@ -388,6 +389,7 @@ class Company extends BaseModel
'e_invoice_certificate_passphrase' => EncryptedCast::class,
'smtp_username' => 'encrypted',
'smtp_password' => 'encrypted',
'e_invoice' => 'object',
];
protected $with = [];

View File

@ -155,6 +155,7 @@ class CompanyGateway extends BaseModel
'b9886f9257f0c6ee7c302f1c74475f6c' => 321, //GoCardless
'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9' => 322,
'80af24a6a691230bbec33e930ab40666' => 323,
'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay
];
protected $touches = [];

View File

@ -28,6 +28,7 @@ use Laracasts\Presenter\PresentableTrait;
* App\Models\Credit
*
* @property int $id
* @property object|null $e_invoice
* @property int $client_id
* @property int $user_id
* @property int|null $assigned_user_id
@ -179,6 +180,7 @@ class Credit extends BaseModel
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'is_amount_discount' => 'bool',
'e_invoice' => 'object',
];

View File

@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* App\Models\Expense
*
* @property int $id
* @property object|null $e_invoice
* @property int|null $created_at
* @property int|null $updated_at
* @property int|null $deleted_at
@ -141,6 +142,7 @@ class Expense extends BaseModel
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'e_invoice' => 'object',
];
protected $touches = [];

View File

@ -32,6 +32,7 @@ use Laracasts\Presenter\PresentableTrait;
* App\Models\Invoice
*
* @property int $id
* @property object|null $e_invoice
* @property int $client_id
* @property int $user_id
* @property int|null $assigned_user_id
@ -209,6 +210,7 @@ class Invoice extends BaseModel
'custom_surcharge_tax2' => 'bool',
'custom_surcharge_tax3' => 'bool',
'custom_surcharge_tax4' => 'bool',
'e_invoice' => 'object',
];
protected $with = [];

View File

@ -83,9 +83,9 @@ class CompanyPresenter extends EntityPresenter
];
if (strlen($settings->company_logo) >= 1 && (strpos($settings->company_logo, 'http') !== false)) {
return "data:image/png;base64, ". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options)));
return "data:image/png;base64,". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options)));
} elseif (strlen($settings->company_logo) >= 1) {
return "data:image/png;base64, ". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options)));
return "data:image/png;base64,". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options)));
} else {
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
}

View File

@ -22,6 +22,7 @@ use Illuminate\Support\Carbon;
* App\Models\PurchaseOrder
*
* @property int $id
* @property object|null $e_invoice
* @property int|null $client_id
* @property int $user_id
* @property int|null $assigned_user_id
@ -187,7 +188,7 @@ class PurchaseOrder extends BaseModel
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'is_amount_discount' => 'bool',
'e_invoice' => 'object',
];
public const STATUS_DRAFT = 1;

View File

@ -27,6 +27,7 @@ use Laracasts\Presenter\PresentableTrait;
* App\Models\Quote
*
* @property int $id
* @property object|null $e_invoice
* @property int $client_id
* @property int $user_id
* @property int|null $assigned_user_id
@ -174,6 +175,7 @@ class Quote extends BaseModel
'deleted_at' => 'timestamp',
'is_deleted' => 'boolean',
'is_amount_discount' => 'bool',
'e_invoice' => 'object',
];
public const STATUS_DRAFT = 1;

View File

@ -150,6 +150,8 @@ class SystemLog extends Model
public const TYPE_PAYPAL_PPCP = 323;
public const TYPE_BTC_PAY = 324;
public const TYPE_QUOTA_EXCEEDED = 400;
public const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -67,7 +67,7 @@ class AuthorizePaymentDriver extends BaseDriver
public function getClientRequiredFields(): array
{
$data = [
['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'],
// ['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'],
['name' => 'client_phone', 'label' => ctrans('texts.phone'), 'type' => 'text', 'validation' => 'required'],
['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required|email:rfc'],
['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'],

View File

@ -0,0 +1,426 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\PayPal;
use Str;
use Carbon\Carbon;
use App\Models\Invoice;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\BaseDriver;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\PayPal\PayPalWebhook;
class PayPalBasePaymentDriver extends BaseDriver
{
use MakesHash;
public $token_billing = true;
public $can_authorise_credit_card = false;
public float $fee = 0;
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
public string $api_endpoint_url = '';
public string $paypal_payment_method = '';
public ?int $gateway_type_id = null;
public mixed $access_token = null;
public ?Carbon $token_expiry = null;
public array $funding_options = [
3 => 'paypal',
1 => 'card',
25 => 'venmo',
29 => 'paypal_advanced_cards',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
public function gatewayTypes()
{
$funding_options =
collect($this->company_gateway->fees_and_limits)
->filter(function ($fee) {
return $fee->is_enabled;
})->map(function ($fee, $key) {
return (int)$key;
})->toArray();
/** Parse funding options and remove card option if advanced cards is enabled. */
if(in_array(1, $funding_options) && in_array(29, $funding_options)){
if (($key = array_search(1, $funding_options)) !== false)
unset($funding_options[$key]);
}
return $funding_options;
}
public function getPaymentMethod($gateway_type_id): int
{
$method = PaymentType::PAYPAL;
match($gateway_type_id) {
"1" => $method = PaymentType::CREDIT_CARD_OTHER,
"3" => $method = PaymentType::PAYPAL,
"25" => $method = PaymentType::VENMO,
"28" => $method = PaymentType::PAY_LATER,
"29" => $method = PaymentType::CREDIT_CARD_OTHER,
};
return $method;
}
public function init()
{
$this->api_endpoint_url = $this->company_gateway->getConfigField('testMode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
$secret = $this->company_gateway->getConfigField('secret');
$client_id = $this->company_gateway->getConfigField('clientId');
if($this->access_token && $this->token_expiry && $this->token_expiry->isFuture()) {
return $this;
}
$response = Http::withBasicAuth($client_id, $secret)
->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded'])
->withQueryParameters(['grant_type' => 'client_credentials'])
->post("{$this->api_endpoint_url}/v1/oauth2/token");
if($response->successful()) {
$this->access_token = $response->json()['access_token'];
$this->token_expiry = now()->addSeconds($response->json()['expires_in'] - 60);
} else {
throw new PaymentFailed('Unable to gain access token from Paypal. Check your configuration', 401);
}
return $this;
}
/**
* getFundingOptions
*
* Hosted fields requires this.
*
* @return string
*/
public function getFundingOptions(): string
{
$enums = [
1 => 'card',
3 => 'paypal',
25 => 'venmo',
28 => 'paylater',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
// 28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
$funding_options = '';
foreach($this->company_gateway->fees_and_limits as $key => $value) {
if($value->is_enabled) {
$funding_options .= $enums[$key].',';
}
}
return rtrim($funding_options, ',');
}
public function getShippingAddress(): ?array
{
return $this->company_gateway->require_shipping_address ?
[
"address" =>
[
"address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
"admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
"postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
],
]
: [
"name" => [
"full_name" => $this->client->present()->name()
]
];
}
public function getBillingAddress(): array
{
return
[
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
];
}
public function getPaymentSource(): array
{
//@todo - roll back here for advanced payments vs hosted card fields.
if($this->gateway_type_id == GatewayType::PAYPAL_ADVANCED_CARDS) {
return [
"card" => [
"attributes" => [
"verification" => [
"method" => "SCA_WHEN_REQUIRED", //SCA_ALWAYS
// "method" => "SCA_ALWAYS", //SCA_ALWAYS
],
"vault" => [
"store_in_vault" => "ON_SUCCESS", //must listen to this webhook - VAULT.PAYMENT-TOKEN.CREATED webhook.
],
],
"experience_context" => [
"shipping_preference" => "SET_PROVIDED_ADDRESS"
],
"stored_credential" => [
// "payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_initiator" => "CUSTOMER", //"" who initiated the transaction?
"payment_type" => "UNSCHEDULED", //UNSCHEDULED
"usage"=> "DERIVED",
],
],
];
}
$order = [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
];
/** If we have a complete address, add it to the order, otherwise leave it blank! */
if(
strlen($this->client->shipping_address1 ?? '') > 2 &&
strlen($this->client->shipping_city ?? '') > 2 &&
strlen($this->client->shipping_state ?? '') >= 2 &&
strlen($this->client->shipping_postal_code ?? '') > 2 &&
strlen($this->client->shipping_country->iso_3166_2 ?? '') >= 2
) {
$order['paypal']['address'] = [
"address_line_1" => $this->client->shipping_address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => $this->client->shipping_city,
"admin_area_1" => $this->client->shipping_state,
"postal_code" => $this->client->shipping_postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
];
}
elseif(
strlen($this->client->address1 ?? '') > 2 &&
strlen($this->client->city ?? '') > 2 &&
strlen($this->client->state ?? '') >= 2 &&
strlen($this->client->postal_code ?? '') > 2 &&
strlen($this->client->country->iso_3166_2 ?? '') >= 2
)
{
$order['paypal']['address'] = [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
];
}
return $order;
}
/**
* Payment method setter
*
* @param mixed $payment_method_id
* @return self
*/
public function setPaymentMethod($payment_method_id): self
{
if(!$payment_method_id) {
return $this;
}
$this->gateway_type_id = $payment_method_id;
$this->paypal_payment_method = $this->funding_options[$payment_method_id];
return $this;
}
public function authorizeView($payment_method)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function authorizeResponse($request)
{
// PayPal doesn't support direct authorization.
return $this;
}
/**
* Generates the gateway request
*
* @param string $uri
* @param string $verb
* @param array $data
* @param ?array $headers
* @return \Illuminate\Http\Client\Response
*/
public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = [])
{
$this->init();
$r = Http::withToken($this->access_token)
->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
if($r->successful()) {
return $r;
}
SystemLogger::dispatch(
['response' => $r->body()],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company ?? $this->company_gateway->company,
);
throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
}
/**
* Generates the request headers
*
* @param array $headers
* @return array
*/
public function getHeaders(array $headers = []): array
{
return array_merge([
'Accept' => 'application/json',
'Content-type' => 'application/json',
'Accept-Language' => 'en_US',
'PayPal-Partner-Attribution-Id' => 'invoiceninja_SP_PPCP',
'PayPal-Request-Id' => Str::uuid()->toString(),
], $headers);
}
/**
* Generates a client token for the payment form.
*
* @return string
*/
public function getClientToken(): string
{
$r = $this->gatewayRequest('/v1/identity/generate-token', 'post', ['body' => '']);
if($r->successful()) {
return $r->json()['client_token'];
}
throw new PaymentFailed('Unable to gain client token from Paypal. Check your configuration', 401);
}
public function auth(): bool
{
try {
$this->init()->getClientToken();
return true;
}
catch(\Exception $e) {
}
return false;
}
public function importCustomers()
{
return true;
}
public function processWebhookRequest(Request $request)
{
$this->init();
PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token);
}
}

View File

@ -21,188 +21,34 @@ use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\PayPal\PayPalWebhook;
use App\PaymentDrivers\PayPal\PayPalBasePaymentDriver;
class PayPalPPCPPaymentDriver extends BaseDriver
class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
{
use MakesHash;
public $token_billing = false;
public $can_authorise_credit_card = false;
private $omnipay_gateway;
private float $fee = 0;
///v1/customer/partners/merchant-accounts/{merchant_id}/capabilities - test if advanced cards is available.
// {
// "capabilities": [
// {
// "name": "ADVANCED_CARD_PAYMENTS",
// "status": "ENABLED"
// },
// {
// "name": "VAULTING",
// "status": "ENABLED"
// }
// ]
// }
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL_PPCP;
private string $api_endpoint_url = '';
private string $paypal_payment_method = '';
private ?int $gateway_type_id = null;
protected mixed $access_token = null;
protected ?Carbon $token_expiry = null;
private array $funding_options = [
3 => 'paypal',
1 => 'card',
25 => 'venmo',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
/**
* Return an array of
* enabled gateway payment methods
*
* @return array
*/
public function gatewayTypes(): array
{
return collect($this->company_gateway->fees_and_limits)
->filter(function ($fee) {
return $fee->is_enabled;
})->map(function ($fee, $key) {
return (int)$key;
})->toArray();
}
private function getPaymentMethod($gateway_type_id): int
{
$method = PaymentType::PAYPAL;
match($gateway_type_id) {
"1" => $method = PaymentType::CREDIT_CARD_OTHER,
"3" => $method = PaymentType::PAYPAL,
"25" => $method = PaymentType::VENMO,
"28" => $method = PaymentType::PAY_LATER,
};
return $method;
}
private function getFundingOptions(): string
{
$enums = [
1 => 'card',
3 => 'paypal',
25 => 'venmo',
28 => 'paylater',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
// 28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
$funding_options = '';
foreach($this->company_gateway->fees_and_limits as $key => $value) {
if($value->is_enabled) {
$funding_options .= $enums[$key].',';
}
}
return rtrim($funding_options, ',');
}
/**
* Initialize the Paypal gateway.
*
* Attempt to generate and return the access token.
*
* @return self
*/
public function init(): self
{
$this->api_endpoint_url = 'https://api-m.paypal.com';
// $this->api_endpoint_url = 'https://api-m.sandbox.paypal.com';
$secret = config('ninja.paypal.secret');
$client_id = config('ninja.paypal.client_id');
if($this->access_token && $this->token_expiry && $this->token_expiry->isFuture()) {
return $this;
}
$response = Http::withBasicAuth($client_id, $secret)
->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded'])
->withQueryParameters(['grant_type' => 'client_credentials'])
->post("{$this->api_endpoint_url}/v1/oauth2/token");
if($response->successful()) {
$this->access_token = $response->json()['access_token'];
$this->token_expiry = now()->addSeconds($response->json()['expires_in'] - 60);
} else {
throw new PaymentFailed('Unable to gain access token from Paypal. Check your configuration', 401);
}
return $this;
}
/**
* Payment method setter
*
* @param mixed $payment_method_id
* @return self
*/
public function setPaymentMethod($payment_method_id): self
{
if(!$payment_method_id) {
return $this;
}
$this->gateway_type_id = $payment_method_id;
$this->paypal_payment_method = $this->funding_options[$payment_method_id];
return $this;
}
public function authorizeView($payment_method)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function authorizeResponse($request)
{
// PayPal doesn't support direct authorization.
return $this;
}
/**
* Checks whether payments are enabled on the account
*
* Checks whether payments are enabled on the merchant account
*
* @return self
*/
private function checkPaymentsReceivable(): self
@ -252,7 +98,10 @@ class PayPalPPCPPaymentDriver extends BaseDriver
$data['merchantId'] = $this->company_gateway->getConfigField('merchantId');
$data['currency'] = $this->client->currency()->code;
return render('gateways.paypal.ppcp.pay', $data);
if($this->gateway_type_id == 29)
return render('gateways.paypal.ppcp.card', $data);
else
return render('gateways.paypal.ppcp.pay', $data);
}
@ -268,6 +117,10 @@ class PayPalPPCPPaymentDriver extends BaseDriver
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true);
if($request->has('token') && strlen($request->input('token')) > 2) {
return $this->processTokenPayment($request, $response);
}
//capture
$orderID = $response['orderID'];
@ -335,7 +188,7 @@ class PayPalPPCPPaymentDriver extends BaseDriver
['response' => $response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
SystemLog::TYPE_PAYPAL_PPCP,
$this->client,
$this->client->company,
);
@ -352,7 +205,7 @@ class PayPalPPCPPaymentDriver extends BaseDriver
['response' => $response],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
SystemLog::TYPE_PAYPAL_PPCP,
$this->client,
$this->client->company,
);
@ -372,74 +225,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver
return $r->json();
}
/**
* Generates a client token for the payment form.
*
* @return string
*/
private function getClientToken(): string
{
$r = $this->gatewayRequest('/v1/identity/generate-token', 'post', ['body' => '']);
if($r->successful()) {
return $r->json()['client_token'];
}
throw new PaymentFailed('Unable to gain client token from Paypal. Check your configuration', 401);
}
/**
* Builds the payment request.
*
* @return array
*/
private function paymentSource(): array
{
/** we only need to support paypal as payment source until as we are only using hosted payment buttons */
return $this->injectPayPalPaymentSource();
}
private function injectPayPalPaymentSource(): array
{
$order = [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
];
if(
strlen($this->client->address1 ?? '') > 2 &&
strlen($this->client->city ?? '') > 2 &&
strlen($this->client->state ?? '') >= 2 &&
strlen($this->client->postal_code ?? '') > 2 &&
strlen($this->client->country->iso_3166_2 ?? '') >= 2
)
{
$order["paypal"]["address"] = $this->getBillingAddress();
}
return $order;
}
/**
* Creates the PayPal Order object
*
* @param array $data
* @return string
*/
private function createOrder(array $data): string
public function createOrder(array $data): string
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
@ -453,7 +245,7 @@ class PayPalPPCPPaymentDriver extends BaseDriver
$order = [
"intent" => "CAPTURE",
"payment_source" => $this->paymentSource(),
"payment_source" => $this->getPaymentSource(),
"purchase_units" => [
[
"custom_id" => $this->payment_hash->hash,
@ -465,7 +257,6 @@ class PayPalPPCPPaymentDriver extends BaseDriver
"payment_instruction" => [
"disbursement_mode" => "INSTANT",
],
$this->getShippingAddress(),
"amount" => [
"value" => (string)$data['amount_with_fee'],
"currency_code" => $this->client->currency()->code,
@ -496,125 +287,76 @@ class PayPalPPCPPaymentDriver extends BaseDriver
$order['purchase_units'][0]["shipping"] = $shipping;
}
if(isset($data['payment_source'])) {
$order['payment_source'] = $data['payment_source'];
}
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
return $r->json()['id'];
}
private function getBillingAddress(): array
{
return
[
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
];
}
private function getShippingAddress(): ?array
{
return $this->company_gateway->require_shipping_address ?
[
"address" =>
[
"address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => strlen($this->client->shipping_city ?? '') > 1 ? $this->client->shipping_city : $this->client->city,
"admin_area_1" => strlen($this->client->shipping_state ?? '') > 1 ? $this->client->shipping_state : $this->client->state,
"postal_code" => strlen($this->client->shipping_postal_code ?? '') > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
],
]
: null;
}
/**
* Generates the gateway request
* processTokenPayment
*
* @param string $uri
* @param string $verb
* @param array $data
* @param ?array $headers
* @return \Illuminate\Http\Client\Response
* With PayPal and token payments, the order needs to be
* deleted and then created with the payment source that
* has been selected by the client.
*
* This method handle the deletion of the current paypal order,
* and the automatic payment of the order with the selected payment source.
*
* @param mixed $request
* @param array $response
* @return void
*/
public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = [])
{
$this->init();
public function processTokenPayment($request, array $response) {
$r = Http::withToken($this->access_token)
->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
$cgt = ClientGatewayToken::where('client_id', $this->client->id)
->where('token', $request['token'])
->firstOrFail();
if($r->successful()) {
return $r;
}
$orderId = $response['orderID'];
$r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']);
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [
"vault_id" => $cgt->token,
"stored_credential" => [
"payment_initiator" => "MERCHANT",
"payment_type" => "UNSCHEDULED",
"usage" => "SUBSEQUENT",
],
],
];
$orderId = $this->createOrder($data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
$response = $r->json();
$data = [
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'],
'gateway_type_id' => $this->gateway_type_id,
];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $r->body()],
['response' => $response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL_PPCP,
$this->client,
$this->client->company,
);
throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
/**
* Generates the request headers
*
* @param array $headers
* @return array
*/
private function getHeaders(array $headers = []): array
{
return array_merge([
'Accept' => 'application/json',
'Content-type' => 'application/json',
'Accept-Language' => 'en_US',
'PayPal-Partner-Attribution-Id' => 'invoiceninja_SP_PPCP',
'PayPal-Request-Id' => Str::uuid()->toString(),
], $headers);
}
public function processWebhookRequest(Request $request)
{
// nlog(json_encode($request->all()));
$this->init();
PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token);
}
public function auth(): bool
{
try {
$this->init()->getClientToken();
return true;
}
catch(\Exception $e) {
}
return false;
}
public function importCustomers()
{
// $response = $this->gatewayRequest('/v1/reporting/transactions', 'get', ['fields' => 'all','page_size' => 500,'start_date' => '2024-02-01T00:00:00-0000', 'end_date' => '2024-03-01T00:00:00-0000']);
// nlog($response->json());
return true;
}
}
}

View File

@ -12,147 +12,22 @@
namespace App\PaymentDrivers;
use Carbon\Carbon;
use Omnipay\Omnipay;
use App\Models\Invoice;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Support\Str;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\PayPal\PayPalBasePaymentDriver;
class PayPalRestPaymentDriver extends BaseDriver
class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
{
use MakesHash;
public $token_billing = false;
public $can_authorise_credit_card = false;
private $omnipay_gateway;
private float $fee = 0;
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
private string $api_endpoint_url = '';
private string $paypal_payment_method = '';
private ?int $gateway_type_id = null;
protected mixed $access_token = null;
protected ?Carbon $token_expiry = null;
private array $funding_options = [
3 => 'paypal',
1 => 'card',
25 => 'venmo',
29 => 'paypal_advanced_cards',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
public function gatewayTypes()
{
$funding_options = [];
foreach ($this->company_gateway->fees_and_limits as $key => $value) {
if ($value->is_enabled) {
$funding_options[] = $key;
}
}
return $funding_options;
}
public function init()
{
$this->api_endpoint_url = $this->company_gateway->getConfigField('testMode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
$secret = $this->company_gateway->getConfigField('secret');
$client_id = $this->company_gateway->getConfigField('clientId');
if($this->access_token && $this->token_expiry && $this->token_expiry->isFuture()) {
return $this;
}
$response = Http::withBasicAuth($client_id, $secret)
->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded'])
->withQueryParameters(['grant_type' => 'client_credentials'])
->post("{$this->api_endpoint_url}/v1/oauth2/token");
if($response->successful()) {
$this->access_token = $response->json()['access_token'];
$this->token_expiry = now()->addSeconds($response->json()['expires_in'] - 60);
} else {
throw new PaymentFailed('Unable to gain access token from Paypal. Check your configuration', 401);
}
return $this;
}
private function getPaymentMethod($gateway_type_id): int
{
$method = PaymentType::PAYPAL;
match($gateway_type_id) {
"1" => $method = PaymentType::CREDIT_CARD_OTHER,
"3" => $method = PaymentType::PAYPAL,
"25" => $method = PaymentType::VENMO,
"28" => $method = PaymentType::PAY_LATER,
"29" => $method = PaymentType::CREDIT_CARD_OTHER,
};
return $method;
}
public function setPaymentMethod($payment_method_id): self
{
if(!$payment_method_id) {
return $this;
}
$this->gateway_type_id = $payment_method_id;
$this->paypal_payment_method = $this->funding_options[$payment_method_id];
return $this;
}
public function authorizeView($payment_method)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function authorizeResponse($request)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function processPaymentView($data)
{
$this->init();
@ -169,110 +44,20 @@ class PayPalRestPaymentDriver extends BaseDriver
$data['gateway_type_id'] = $this->gateway_type_id;
$data['currency'] = $this->client->currency()->code;
// return render('gateways.paypal.ppcp.card', $data);
return render('gateways.paypal.pay', $data);
if($this->gateway_type_id == 29)
return render('gateways.paypal.ppcp.card', $data);
else
return render('gateways.paypal.pay', $data);
}
private function getFundingOptions(): string
{
$enums = [
3 => 'paypal',
1 => 'card',
25 => 'venmo',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
// 28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
$funding_options = '';
foreach($this->company_gateway->fees_and_limits as $key => $value) {
if($value->is_enabled) {
$funding_options .= $enums[$key].',';
}
}
return rtrim($funding_options, ',');
}
public function processTokenPayment($request, array $response) {
$cgt = ClientGatewayToken::where('client_id', $this->client->id)
->where('token', $request['token'])
->firstOrFail();
nlog("process token");
nlog($request->all());
nlog($response);
$orderId = $response['orderID'];
$r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']);
nlog($r);
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [
"vault_id" => $cgt->token,
"stored_credential" => [
"payment_initiator" => "MERCHANT",
"payment_type" => "UNSCHEDULED",
"usage" => "SUBSEQUENT",
// "previous_transaction_reference" => $cgt->gateway_customer_reference,
],
],
];
$orderId = $this->createOrder($data);
nlog("post order creation");
nlog($orderId);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
nlog($r);
$response = $r->json();
nlog($response);
$data = [
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'],
'gateway_type_id' => $this->gateway_type_id,
];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
/**
* processPaymentResponse
*
* @param mixed $request
* @return void
*/
public function processPaymentResponse($request)
{
@ -280,12 +65,10 @@ return render('gateways.paypal.pay', $data);
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true);
nlog($request->all());
if($request->has('token') && strlen($request->input('token')) > 2)
return $this->processTokenPayment($request, $response);
// nlog($response);
//capture
$orderID = $response['orderID'];
@ -339,6 +122,9 @@ return render('gateways.paypal.pay', $data);
$response = $r;
nlog("Process response =>");
nlog($response->json());
if(isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
return $this->createNinjaPayment($request, $response);
@ -367,8 +153,6 @@ return render('gateways.paypal.pay', $data);
private function createNinjaPayment($request, $response) {
nlog($response->json());
$data = [
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
@ -401,7 +185,6 @@ return render('gateways.paypal.pay', $data);
$data['token'] = $token;
$data['payment_method_id'] = GatewayType::PAYPAL_ADVANCED_CARDS;
$data['payment_meta'] = $payment_meta;
// $data['payment_method_id'] = GatewayType::CREDIT_CARD;
$additional['gateway_customer_reference'] = $gateway_customer_reference;
@ -423,116 +206,7 @@ return render('gateways.paypal.pay', $data);
}
private function getClientToken(): string
{
$r = $this->gatewayRequest('/v1/identity/generate-token', 'post', ['body' => '']);
if($r->successful()) {
return $r->json()['client_token'];
}
throw new PaymentFailed('Unable to gain client token from Paypal. Check your configuration', 401);
}
private function getPaymentSource(): array
{
//@todo - roll back here for advanced payments vs hosted card fields.
if($this->gateway_type_id == GatewayType::PAYPAL_ADVANCED_CARDS) {
return [
"card" => [
"attributes" => [
"verification" => [
"method" => "SCA_WHEN_REQUIRED", //SCA_ALWAYS
// "method" => "SCA_ALWAYS", //SCA_ALWAYS
],
"vault" => [
"store_in_vault" => "ON_SUCCESS", //must listen to this webhook - VAULT.PAYMENT-TOKEN.CREATED webhook.
],
],
"experience_context" => [
"shipping_preference" => "SET_PROVIDED_ADDRESS"
],
// "name" => $this->client->present()->primary_contact_name(),
// "email_address" => $this->client->present()->email(),
// "address" => [
// "address_line_1" => $this->client->address1,
// "address_line_2" => $this->client->address2,
// "admin_area_2" => $this->client->city,
// "admin_area_1" => $this->client->state,
// "postal_code" => $this->client->postal_code,
// "country_code" => $this->client->country->iso_3166_2,
// ],
// "experience_context" => [
// "user_action" => "PAY_NOW"
// ],
"stored_credential" => [
// "payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_initiator" => "CUSTOMER", //"" who initiated the transaction?
"payment_type" => "UNSCHEDULED", //UNSCHEDULED
"usage"=> "DERIVED",
],
],
];
}
$order = [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
];
/** If we have a complete address, add it to the order, otherwise leave it blank! */
if(
strlen($this->client->shipping_address1 ?? '') > 2 &&
strlen($this->client->shipping_city ?? '') > 2 &&
strlen($this->client->shipping_state ?? '') >= 2 &&
strlen($this->client->shipping_postal_code ?? '') > 2 &&
strlen($this->client->shipping_country->iso_3166_2 ?? '') >= 2
) {
$order['paypal']['address'] = [
"address_line_1" => $this->client->shipping_address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => $this->client->shipping_city,
"admin_area_1" => $this->client->shipping_state,
"postal_code" => $this->client->shipping_postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
];
}
elseif(
strlen($this->client->address1 ?? '') > 2 &&
strlen($this->client->city ?? '') > 2 &&
strlen($this->client->state ?? '') >= 2 &&
strlen($this->client->postal_code ?? '') > 2 &&
strlen($this->client->country->iso_3166_2 ?? '') >= 2
)
{
$order['paypal']['address'] = [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
];
}
return $order;
}
private function createOrder(array $data): string
public function createOrder(array $data): string
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
@ -576,126 +250,81 @@ return render('gateways.paypal.pay', $data);
]
];
if($shipping = $this->getShippingAddress()) {
$order['purchase_units'][0]["shipping"] = $shipping;
}
if(isset($data['payment_source']))
$order['payment_source'] = $data['payment_source'];
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
return $r->json()['id'];
}
private function getShippingAddress(): ?array
{
return $this->company_gateway->require_shipping_address ?
[
"address" =>
[
"address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
"admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
"postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
],
]
/**
* processTokenPayment
*
* With PayPal and token payments, the order needs to be
* deleted and then created with the payment source that
* has been selected by the client.
*
* This method handle the deletion of the current paypal order,
* and the automatic payment of the order with the selected payment source.
*
* @param mixed $request
* @param array $response
* @return void
*/
public function processTokenPayment($request, array $response) {
: [
"name" => [
"full_name" => $this->client->present()->name()
]
$cgt = ClientGatewayToken::where('client_id', $this->client->id)
->where('token', $request['token'])
->firstOrFail();
$orderId = $response['orderID'];
$r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']);
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [
"vault_id" => $cgt->token,
"stored_credential" => [
"payment_initiator" => "MERCHANT",
"payment_type" => "UNSCHEDULED",
"usage" => "SUBSEQUENT",
],
],
];
$orderId = $this->createOrder($data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
$response = $r->json();
$data = [
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'],
'gateway_type_id' => $this->gateway_type_id,
];
}
/**
* Generates the gateway request
*
* @param string $uri
* @param string $verb
* @param array $data
* @param ?array $headers
* @return \Illuminate\Http\Client\Response
*/
public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = [])
{
$this->init();
$r = Http::withToken($this->access_token)
->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
if($r->successful()) {
return $r;
}
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $r->body()],
['response' => $response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company ?? $this->company_gateway->company,
$this->client->company,
);
throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
private function getHeaders(array $headers = []): array
{
return array_merge([
'Accept' => 'application/json',
'Content-type' => 'application/json',
'Accept-Language' => 'en_US',
'PayPal-Partner-Attribution-Id' => 'invoiceninja_SP_PPCP',
'PayPal-Request-Id' => Str::uuid()->toString(),
], $headers);
}
private function feeCalc($invoice, $invoice_total)
{
$invoice->service()->removeUnpaidGatewayFees();
$invoice = $invoice->fresh();
$balance = floatval($invoice->balance);
$_updated_invoice = $invoice->service()->addGatewayFee($this->company_gateway, GatewayType::PAYPAL, $invoice_total)->save();
if (floatval($_updated_invoice->balance) > $balance) {
$fee = floatval($_updated_invoice->balance) - $balance;
$this->payment_hash->fee_total = $fee;
$this->payment_hash->save();
return $fee;
}
return 0;
}
public function auth(): bool
{
try {
$this->init()->getClientToken();
return true;
}
catch(\Exception $e) {
}
return false;
}
public function importCustomers()
{
return true;
}
}

View File

@ -150,7 +150,7 @@ class TaskRepository extends BaseRepository
{
if(isset($time_log[0][0])) {
return \Carbon\Carbon::createFromTimestamp($time_log[0][0]);
return \Carbon\Carbon::createFromTimestamp($time_log[0][0])->addSeconds($task->company->utc_offset());
}
return null;

View File

@ -314,7 +314,7 @@ class Email implements ShouldQueue
$this->logMailError($e->getMessage(), $this->company->clients()->first());
$this->cleanUpMailers();
$this->entityEmailFailed($message);
$this->entityEmailFailed($message);
return;
}

View File

@ -211,6 +211,7 @@ class CompanyTransformer extends EntityTransformer
'smtp_password' => $company->smtp_password ? '********' : '',
'smtp_local_domain' => (string)$company->smtp_local_domain ?? '',
'smtp_verify_peer' => (bool)$company->smtp_verify_peer,
'e_invoice' => $company->e_invoice ?: new \stdClass(),
];
}

View File

@ -133,6 +133,7 @@ class CreditTransformer extends EntityTransformer
'subscription_id' => $this->encodePrimaryKey($credit->subscription_id),
'invoice_id' => $credit->invoice_id ? $this->encodePrimaryKey($credit->invoice_id) : '',
'tax_info' => $credit->tax_data ?: new \stdClass(),
'e_invoice' => $credit->e_invoice ?: new \stdClass(),
];
}

View File

@ -148,6 +148,8 @@ class ExpenseTransformer extends EntityTransformer
'uses_inclusive_taxes' => (bool) $expense->uses_inclusive_taxes,
'calculate_tax_by_amount' => (bool) $expense->calculate_tax_by_amount,
'entity_type' => 'expense',
'e_invoice' => $expense->e_invoice ?: new \stdClass(),
];
}
}

View File

@ -158,6 +158,8 @@ class InvoiceTransformer extends EntityTransformer
'subscription_id' => $this->encodePrimaryKey($invoice->subscription_id),
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'tax_info' => $invoice->tax_data ?: new \stdClass(),
'e_invoice' => $invoice->e_invoice ?: new \stdClass(),
];
if (request()->has('reminder_schedule') && request()->query('reminder_schedule') == 'true') {

View File

@ -150,6 +150,8 @@ class PurchaseOrderTransformer extends EntityTransformer
'expense_id' => $this->encodePrimaryKey($purchase_order->expense_id),
'currency_id' => $purchase_order->currency_id ? (string) $purchase_order->currency_id : '',
'tax_info' => $purchase_order->tax_data ?: new \stdClass(),
'e_invoice' => $purchase_order->e_invoice ?: new \stdClass(),
];
}
}

View File

@ -149,6 +149,8 @@ class QuoteTransformer extends EntityTransformer
'project_id' => $this->encodePrimaryKey($quote->project_id),
'subscription_id' => $this->encodePrimaryKey($quote->subscription_id),
'tax_info' => $quote->tax_data ?: new \stdClass(),
'e_invoice' => $quote->e_invoice ?: new \stdClass(),
];
}
}

View File

@ -561,10 +561,19 @@ class HtmlEngine
$data['$spc_qr_code'] = ['value' => $this->company->present()->getSpcQrCode($this->client->currency()->code, $this->entity->number, $this->entity->balance, $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client)), 'label' => ''];
$logo = $this->company->present()->logo_base64($this->settings);
if(Ninja::isHosted())
$logo = $this->company->present()->logo($this->settings);
else
$logo = $this->company->present()->logo_base64($this->settings);
$logo_url = $this->company->present()->logo($this->settings);
$data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')];
$data['$company_logo'] = &$data['$company.logo'];
$data['$company.logo_url'] = ['value' => $logo_url ?: ' ', 'label' => ctrans('texts.logo')];
$data['$company1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company1', $this->settings->custom_value1, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company1')];
$data['$company2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company2', $this->settings->custom_value2, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company2')];
$data['$company3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'company3', $this->settings->custom_value3, $this->client) ?: ' ', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'company3')];

44
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "da485c7cec773404ffe59d450b6505cf",
"content-hash": "1356155e46e797b140685c105df97b8e",
"packages": [
{
"name": "adrienrn/php-mimetyper",
@ -3221,26 +3221,26 @@
},
{
"name": "firebase/php-jwt",
"version": "v6.10.0",
"version": "v6.10.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff"
"reference": "500501c2ce893c824c801da135d02661199f60c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff",
"reference": "a49db6f0a5033aef5143295342f1c95521b075ff",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
"reference": "500501c2ce893c824c801da135d02661199f60c5",
"shasum": ""
},
"require": {
"php": "^7.4||^8.0"
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^6.5||^7.4",
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^1.0||^2.0",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
@ -3278,9 +3278,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.10.0"
"source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
},
"time": "2023-12-01T16:26:39+00:00"
"time": "2024-05-18T18:05:11+00:00"
},
{
"name": "fruitcake/php-cors",
@ -3602,23 +3602,23 @@
},
{
"name": "google/apiclient-services",
"version": "v0.355.0",
"version": "v0.356.0",
"source": {
"type": "git",
"url": "https://github.com/googleapis/google-api-php-client-services.git",
"reference": "235e6a45ecafd77accc102b5ab6d529aab54da23"
"reference": "8e22b0a6f661f2db3f99abb6ee5a1dcf28d370e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/235e6a45ecafd77accc102b5ab6d529aab54da23",
"reference": "235e6a45ecafd77accc102b5ab6d529aab54da23",
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/8e22b0a6f661f2db3f99abb6ee5a1dcf28d370e7",
"reference": "8e22b0a6f661f2db3f99abb6ee5a1dcf28d370e7",
"shasum": ""
},
"require": {
"php": "^7.4||^8.0"
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7||^8.5.13"
"phpunit/phpunit": "^9.6"
},
"type": "library",
"autoload": {
@ -3640,9 +3640,9 @@
],
"support": {
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.355.0"
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.356.0"
},
"time": "2024-05-11T01:02:11+00:00"
"time": "2024-05-18T01:10:18+00:00"
},
{
"name": "google/auth",
@ -5034,12 +5034,12 @@
"source": {
"type": "git",
"url": "https://github.com/invoiceninja/einvoice.git",
"reference": "6028038ff94e6c0090ba5c444bd0be56a65bc0bc"
"reference": "39aec367c528ba66d923dc1d9e5c96f673bb46c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/6028038ff94e6c0090ba5c444bd0be56a65bc0bc",
"reference": "6028038ff94e6c0090ba5c444bd0be56a65bc0bc",
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/39aec367c528ba66d923dc1d9e5c96f673bb46c7",
"reference": "39aec367c528ba66d923dc1d9e5c96f673bb46c7",
"shasum": ""
},
"require": {
@ -5075,7 +5075,7 @@
"source": "https://github.com/invoiceninja/einvoice/tree/main",
"issues": "https://github.com/invoiceninja/einvoice/issues"
},
"time": "2024-05-18T12:35:18+00:00"
"time": "2024-05-20T11:42:32+00:00"
},
{
"name": "invoiceninja/inspector",

View File

@ -27,6 +27,7 @@ class AccountFactory extends Factory
'default_company_id' => 1,
'key' => Str::random(32),
'report_errors' => 1,
'referral_code' => Str::lower(Str::random(32)),
];
}
}

View File

@ -12,24 +12,28 @@ return new class extends Migration
*/
public function up(): void
{
$gateway = new Gateway;
$gateway->name = 'BTCPay';
$gateway->key = 'vpyfbmdrkqcicpkjqdusgjfluebftuva';
$gateway->provider = 'BTCPay';
$gateway->is_offsite = true;
if(!Gateway::find(62))
{
$gateway = new Gateway;
$gateway->id = 62;
$gateway->name = 'BTCPay';
$gateway->key = 'vpyfbmdrkqcicpkjqdusgjfluebftuva';
$gateway->provider = 'BTCPay';
$gateway->is_offsite = true;
$btcpayFieds = new \stdClass;
$btcpayFieds->btcpayUrl = "";
$btcpayFieds->apiKey = "";
$btcpayFieds->storeId = "";
$btcpayFieds->webhookSecret = "";
$gateway->fields = \json_encode($btcpayFieds);
$btcpayFieds = new \stdClass;
$btcpayFieds->btcpayUrl = "";
$btcpayFieds->apiKey = "";
$btcpayFieds->storeId = "";
$btcpayFieds->webhookSecret = "";
$gateway->fields = \json_encode($btcpayFieds);
$gateway->visible = true;
$gateway->site_url = 'https://btcpayserver.org';
$gateway->default_gateway_type_id = GatewayType::CRYPTO;
$gateway->save();
$gateway->visible = true;
$gateway->site_url = 'https://btcpayserver.org';
$gateway->default_gateway_type_id = GatewayType::CRYPTO;
$gateway->save();
}
}
/**
@ -39,4 +43,4 @@ return new class extends Migration
{
//
}
};
};

View File

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('invoices', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('quotes', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('credits', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('expenses', function (Blueprint $table) {
$table->mediumText('e_invoice')->nullable();
});
Schema::table('accounts', function (Blueprint $table) {
$table->integer('email_quota')->default(20)->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -252,7 +252,7 @@ class RandomDataSeeder extends Seeder
$invoice->service()->createInvitations()->markSent()->save();
$invoice->ledger()->updateInvoiceBalance($invoice->balance);
$invoice->ledger()->update_invoiceBalance($invoice->balance);
if (rand(0, 1)) {
$payment = Payment::create([
@ -277,7 +277,7 @@ class RandomDataSeeder extends Seeder
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars()));
// $payment->service()->updateInvoicePayment($payment_hash);
// $payment->service()->update_invoicePayment($payment_hash);
}
});

View File

@ -5320,6 +5320,11 @@ $lang = array(
'task_round_to_nearest' => 'Round To Nearest',
'bulk_updated' => 'Successfully updated data',
'bulk_update' => 'Bulk Update',
'calculate' => 'Calculate',
'sum' => 'Sum',
'money' => 'Money',
'web_app' => 'Web App',
'desktop_app' => 'Desktop App',
);
return $lang;

View File

@ -5297,6 +5297,26 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'local_domain_help' => 'Domaine EHLO (facultatif)',
'port_help' => 'ex. 25,587,465',
'host_help' => 'ex. smtp.gmail.com',
'always_show_required_fields' => 'Permet l\'affichage des champs requis d\'un formulaire',
'always_show_required_fields_help' => 'Affiche toujours les champs requis d\'un formulaire au paiement',
'advanced_cards' => 'Cartes avancées',
'activity_140' => 'État de compte envoyé à :client',
'invoice_net_amount' => 'Montant net de la facture',
'round_to_minutes' => 'Arrondir aux minutes',
'1_minute' => '1 minute',
'5_minutes' => '5 minutes',
'15_minutes' => '15 minutes',
'30_minutes' => '30 minutes',
'1_hour' => '1 heure',
'1_day' => '1 jour',
'round_tasks' => 'Arrondir les tâches',
'round_tasks_help' => 'Arrondir les intervales à la sauvegarde des tâches',
'direction' => 'Direction',
'round_up' => 'Arrondir à hausse',
'round_down' => 'Arrondir à la baisse',
'task_round_to_nearest' => 'Arrondir au plus près',
'bulk_updated' => 'Les données ont été mises à jour',
'bulk_update' => 'Mise à jour groupée',
);
return $lang;

View File

@ -700,7 +700,7 @@ $lang = array(
'total_invoiced' => 'Total Invoiced',
'open_balance' => 'Open Balance',
'verify_email' => 'Please visit the link in the account confirmation email to verify your email address.',
'basic_settings' => 'Basic Settings',
'basic_settings' => '基本設定',
'pro' => 'Pro',
'gateways' => 'ペイメントゲートウェイ',
'next_send_on' => 'Send Next: :date',
@ -1344,7 +1344,7 @@ $lang = array(
'auto_bill_payment_method_credit_card' => 'クレジットカード',
'auto_bill_payment_method_paypal' => 'PayPal アカウント',
'auto_bill_notification_placeholder' => 'この請求書は、期日にて登録されているクレジットカードに自動的に請求されます。',
'payment_settings' => 'Payment Settings',
'payment_settings' => '支払い設定',
'on_send_date' => '発送日',
'on_due_date' => '支払期日',
@ -5300,6 +5300,26 @@ $lang = array(
'local_domain_help' => 'EHLO domain (optional)',
'port_help' => 'ie. 25,587,465',
'host_help' => 'ie. smtp.gmail.com',
'always_show_required_fields' => 'Allows show required fields form',
'always_show_required_fields_help' => 'Displays the required fields form always at checkout',
'advanced_cards' => 'Advanced Cards',
'activity_140' => 'Statement sent to :client',
'invoice_net_amount' => 'Invoice Net Amount',
'round_to_minutes' => 'Round To Minutes',
'1_minute' => '1 Minute',
'5_minutes' => '5 Minutes',
'15_minutes' => '15 Minutes',
'30_minutes' => '30 Minutes',
'1_hour' => '1 Hour',
'1_day' => '1 Day',
'round_tasks' => 'Round Tasks',
'round_tasks_help' => 'Round time intervals when saving tasks',
'direction' => 'Direction',
'round_up' => 'Round Up',
'round_down' => 'Round Down',
'task_round_to_nearest' => 'Round To Nearest',
'bulk_updated' => 'Successfully updated data',
'bulk_update' => 'Bulk Update',
);
return $lang;

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
]
},
"resources/js/app.js": {
"file": "assets/app-bfac6a32.js",
"file": "assets/app-a52d5f77.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"

View File

@ -171,15 +171,10 @@ class SquareCreditCard {
document.querySelector('input[name=token]').value = '';
});
// let toggleWithToken = document.querySelector(
// '.toggle-payment-with-token'
// );
// if (!toggleWithToken) {
document.getElementById('loader').classList.add('hidden');
document.getElementById('payment-list').classList.remove('hidden');
document.getElementById('toggle-payment-with-credit-card')?.click();
// }
});
}

View File

@ -1,5 +1,18 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title' => ''])
@php
$gateway_instance = $gateway instanceof \App\Models\CompanyGateway ? $gateway : $gateway->company_gateway;
$token_billing_string = 'true';
if($gateway_instance->token_billing == 'off' || $gateway_instance->token_billing == 'optin'){
$token_billing_string = 'false';
}
if (isset($pre_payment) && $pre_payment == '1' && isset($is_recurring) && $is_recurring == '1') {
$token_billing_string = 'true';
}
@endphp
@section('gateway_head')
@endsection
@ -12,7 +25,7 @@
<input type="hidden" name="gateway_type_id" id="gateway_type_id" value="{{ $gateway_type_id }}">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
<input type="hidden" name="store_card" id="store_card">
<input type="hidden" name="store_card" id="store_card" value="{{ $token_billing_string }}">
<input type="hidden" name="token" value="" id="token">
</form>
@ -44,6 +57,7 @@
<div id="checkout-form">
<!-- Containers for Card Fields hosted by PayPal -->
<div id="card-number-field-container"></div>
<div id="card-name-field-container"></div>
<div class="expcvv" style="display:flex;">
<div id="card-expiry-field-container" style="width:50%"></div>
<div id="card-cvv-field-container" style="width:50%"></div>
@ -62,8 +76,12 @@
@push('footer')
<link rel="stylesheet" type="text/css" href=https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css />
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&components=card-fields" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
@if(isset($merchantId))
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&merchantId={!! $merchantId !!}&components=card-fields" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
@else
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&components=card-fields" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
@endif
<script>
const clientId = "{{ $client_id }}";
@ -129,9 +147,6 @@
},
onError: function(error) {
// console.log("on error")
// console.log(error);
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
@ -144,6 +159,9 @@
// Render each field after checking for eligibility
if (cardField.isEligible()) {
const nameField = cardField.NameField();
nameField.render("#card-name-field-container");
const numberField = cardField.NumberField({
inputEvents: {
@ -152,6 +170,7 @@
}
},
});
numberField.render("#card-number-field-container");
const cvvField = cardField.CVVField({

View File

@ -12,6 +12,8 @@
namespace Tests\Integration\Einvoice;
use Tests\TestCase;
use Sabre\Xml\Reader;
use Sabre\Xml\Service;
use Invoiceninja\Einvoice\Models\FACT1\Invoice;
/**
@ -20,54 +22,182 @@ use Invoiceninja\Einvoice\Models\FACT1\Invoice;
class FACT1Test extends TestCase
{
public array $set = [];
protected function setUp(): void
{
parent::setUp();
}
public function testValidationFact1()
{
$files = [
'tests/Integration/Einvoice/samples/fact1.xml',
'tests/Integration/Einvoice/samples/fact1_no_prefixes.xml',
];
foreach($files as $f) {
$xmlstring = file_get_contents($f);
$xml = file_get_contents($f);
// nlog($xmlstring);
$xml = simplexml_load_string($xmlstring, "SimpleXMLElement");
$xml = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA);
$json = json_encode($xml);
$payload = json_decode($json, true);
$array = json_decode($json, true);
nlog($xml);
nlog($payload);
$validation_array = false;
try {
$rules = Invoice::getValidationRules($payload);
nlog($rules);
$i = Invoice::from($array);
$rules = Invoice::getValidationRules($array);
$this->assertIsArray($rules);
$this->assertIsArray($rules);
$payload = Invoice::from($payload)->toArray();
nlog($payload);
$this->assertIsArray($payload);
$validation_array = Invoice::validate($payload);
$this->assertIsArray($validation_array);
} catch(\Illuminate\Validation\ValidationException $e) {
nlog($e->errors());
}
$validation_array = Invoice::validate($array);
$this->assertIsArray($validation_array);
}
}
public function removeNamespacesFromArray($data)
{
if (is_array($data)) {
foreach ($data as &$item) {
if (isset($item['name'])) {
// Remove the namespace from the name
$item['name'] = preg_replace('/^\{\}(.+)/', '$1', $item['name']);
}
if (isset($item['value']) && is_array($item['value'])) {
// Recursively process child elements
$item['value'] = $this->removeNamespacesFromArray($item['value']);
}
if (isset($item['attributes'])) {
unset($item['attributes']);
}
}
}
return $data;
}
// function convertToKeyValue($data)
// {
// $result = [];
// foreach ($data as $item) {
// // Remove namespace prefix if present
// $name = preg_replace('/^\{\}(.+)/', '$1', $item['name']);
// $result[$name] = $item['value'];
// }
// return $result;
// }
// public function keyValueDeserializer(Reader $reader)
// {
// $values = [];
// $reader->read();
// $reader->next();
// foreach ($reader->parseGetElements() as $element) {
// // Strip the namespace prefix
// echo "merp".PHP_EOL;
// $name = preg_replace('/^\{\}.*/', '', $element['name']);
// $values[$name] = $element['value'];
// }
// return $values;
// }
// public function testFactToArray()
// {
// $xml = file_get_contents('tests/Integration/Einvoice/samples/fact1_no_prefixes.xml');
// $service = new Service();
// // $service->elementMap = [
// // '{}' => 'Sabre\Xml\Deserializer\keyValue',
// // ];
// // $service->elementMap = [
// // '{}*' => function (Reader $reader) use ($service) {
// // return $this->keyValueDeserializer($reader);
// // }
// // ];
// $result = $this->removeNamespacesFromArray($service->parse($xml));
// // Convert parsed XML to key-value array
// if (isset($result['value']) && is_array($result['value'])) {
// $keyValueArray = $this->convertToKeyValue($result['value']);
// } else {
// $keyValueArray = [];
// }
// // Output the result
// nlog($keyValueArray);
// // nlog($cleanedArray);
// nlog($service->parse($xml));
// }
// Output the result
// ($xmlWithoutNamespaces);
// $reader = new Reader();
// $service = new Service();
// $service->elementMap = [
// '*' => 'Sabre\Xml\Deserializer\keyValue',
// ];
// nlog($service->parse($xmlstring));
// $payload ='';
// // $reader->xml($xmlstring);
// // $payload = $reader->parse();
// // nlog($payload);
// $validation_array = false;
// try {
// $rules = Invoice::getValidationRules($payload);
// nlog($rules);
// $this->assertIsArray($rules);
// $payload = Invoice::from($payload)->toArray();
// nlog($payload);
// $this->assertIsArray($payload);
// $validation_array = Invoice::validate($payload);
// $this->assertIsArray($validation_array);
// } catch(\Illuminate\Validation\ValidationException $e) {
// nlog($e->errors());
// }
// $this->assertIsArray($validation_array);
// }
// private function extractName($name): string
// {
// $pattern = '/\{[^{}]*\}([^{}]*)/';
// if (preg_match($pattern, $name, $matches)) {
// $extracted = $matches[1];
// return $extracted;
// }
// return $name;
// }
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,113 @@
<?xml version="1.0"?>
<Invoice>
<UBLVersionID>2.1</UBLVersionID>
<CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</CustomizationID>
<ID>ABC 0020</ID>
<IssueDate>2024-01-01</IssueDate>
<DueDate>2024-01-15</DueDate>
<InvoiceTypeCode>384</InvoiceTypeCode>
<DocumentCurrencyCode>RON</DocumentCurrencyCode>
<TaxCurrencyCode>RON</TaxCurrencyCode>
<AccountingSupplierParty>
<Party>
<PartyIdentification>
<ID>234234234</ID>
</PartyIdentification>
<PostalAddress>
<StreetName>This can be the full address , not just the street and street nr.</StreetName>
<CityName>SECTOR2</CityName>
<CountrySubentity>RO-B</CountrySubentity>
<Country>
<IdentificationCode>RO</IdentificationCode>
</Country>
</PostalAddress>
<PartyTaxScheme>
<CompanyID>RO234234234</CompanyID>
<TaxScheme>
<ID>VAT</ID>
</TaxScheme>
</PartyTaxScheme>
<PartyLegalEntity>
<RegistrationName>Some Copany Name</RegistrationName>
<CompanyID>J40/2222/2009</CompanyID>
</PartyLegalEntity>
<Contact>
<Name>Someone</Name>
<Telephone>88282819832</Telephone>
<ElectronicMail>some@email.com</ElectronicMail>
</Contact>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyIdentification>
<ID>646546549</ID>
</PartyIdentification>
<PostalAddress>
<StreetName>This can be the full address , not just the street and street nr.</StreetName>
<CityName>SECTOR3</CityName>
<CountrySubentity>RO-B</CountrySubentity>
<Country>
<IdentificationCode>RO</IdentificationCode>
</Country>
</PostalAddress>
<PartyLegalEntity>
<RegistrationName>Some Comapny</RegistrationName>
<CompanyID>646546549</CompanyID>
</PartyLegalEntity>
<Contact>
<Name>Someone</Name>
<Telephone>88282819832</Telephone>
<ElectronicMail>some@email.com</ElectronicMail>
</Contact>
</Party>
</AccountingCustomerParty>
<PaymentMeans>
<PaymentMeansCode>42</PaymentMeansCode>
<PayeeFinancialAccount>
<ID>some account nr</ID>
<Name>Bank name</Name>
</PayeeFinancialAccount>
</PaymentMeans>
<TaxTotal>
<TaxAmount currencyID="RON">63.65</TaxAmount>
<TaxSubtotal>
<TaxableAmount currencyID="RON">335.00</TaxableAmount>
<TaxAmount currencyID="RON">63.65</TaxAmount>
<TaxCategory>
<ID>S</ID> // this is a speciffic identifier for the VAT type <Percent>
19</Percent>
<TaxScheme>
<ID>VAT</ID>
</TaxScheme>
</TaxCategory>
</TaxSubtotal>
</TaxTotal>
<LegalMonetaryTotal>
<LineExtensionAmount currencyID="RON">335.00</LineExtensionAmount>
<TaxExclusiveAmount currencyID="RON">335.00</TaxExclusiveAmount>
<TaxInclusiveAmount currencyID="RON">398.65</TaxInclusiveAmount>
<AllowanceTotalAmount currencyID="RON">0.00</AllowanceTotalAmount>
<PayableAmount currencyID="RON">398.65</PayableAmount>
</LegalMonetaryTotal>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity unitCode="H87">1</InvoicedQuantity> // unitcode is a speciffic
identifier for the type of product <LineExtensionAmount currencyID="RON">
335.00</LineExtensionAmount>
<Item>
<Description>Some Description</Description>
<Name>Some product</Name>
<ClassifiedTaxCategory>
<ID>S</ID> // this is a speciffic identifier for the VAT type <Percent>
19</Percent>
<TaxScheme>
<ID>VAT</ID>
</TaxScheme>
</ClassifiedTaxCategory>
</Item>
<Price>
<PriceAmount currencyID="RON">335</PriceAmount>
</Price>
</InvoiceLine>
</Invoice>