Merge branch 'payment-driver-rotessa' into v5-develop

Signed-off-by: Kendall Arneaud <kendall.arneaud@gmail.com>
This commit is contained in:
Kendall Arneaud 2024-06-26 21:38:13 -04:00 committed by GitHub
commit 96c7a475b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 2562 additions and 3 deletions

View File

@ -0,0 +1,55 @@
<?php
namespace App\DataProviders;
final class CAProvinces {
/**
* The provinces and territories of Canada
*
* @var array
*/
protected static $provinces = [
'AB' => 'Alberta',
'BC' => 'British Columbia',
'MB' => 'Manitoba',
'NB' => 'New Brunswick',
'NL' => 'Newfoundland And Labrador',
'NS' => 'Nova Scotia',
'ON' => 'Ontario',
'PE' => 'Prince Edward Island',
'QC' => 'Quebec',
'SK' => 'Saskatchewan',
'NT' => 'Northwest Territories',
'NU' => 'Nunavut',
'YT' => 'Yukon'
];
/**
* Get the name of the province or territory for a given abbreviation.
*
* @param string $abbreviation
* @return string
*/
public static function getName($abbreviation) {
return self::$provinces[$abbreviation];
}
/**
* Get all provinces and territories.
*
* @return array
*/
public static function get() {
return self::$provinces;
}
/**
* Get the abbreviation for a given province or territory name.
*
* @param string $name
* @return string
*/
public static function getAbbreviation($name) {
return array_search(ucwords($name), self::$provinces);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\DataProviders;
use Omnipay\Rotessa\Object\Frequency;
final class Frequencies
{
public static function get() : array {
return Frequency::getTypes();
}
public static function getFromType() {
}
public static function getOnePayment() {
return Frequency::ONCE;
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Http\ViewComposers\Components;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
use Illuminate\View\Component;
use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// Contact Component
class ContactComponent extends Component
{
public function __construct(ClientContact $contact) {
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' =>$contact->client->phone,
'custom_identifier' => $contact->client->number,
'name' =>$contact->client->name,
'id' => null
] )->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) );
}
private $fields = [
'name',
'email',
'home_phone',
'phone',
'custom_identifier',
'customer_type' ,
'id'
];
private $defaults = [
'customer_type' => "Business",
'customer_identifier' => null,
'id' => null
];
public function render()
{
return render('gateways.rotessa.components.contact', array_merge($this->defaults, $this->attributes->getAttributes() ) );
}
}
// Address Component
class AddressComponent extends Component
{
private $fields = [
'address_1',
'address_2',
'city',
'postal_code',
'province_code',
'country'
];
private $defaults = [
'country' => 'US'
];
public array $address;
public function __construct(array $address) {
$this->address = $address;
if(strlen($this->address['state']) > 2 ) {
$this->address['state'] = $this->address['country'] == 'US' ? array_search($this->address['state'], USStates::$states) : CAProvinces::getAbbreviation($this->address['state']);
}
$this->attributes = $this->newAttributeBag(
Arr::only(Arr::mapWithKeys($this->address, function ($item, $key) {
return in_array($key, ['address1','address2','state'])?[ (['address1'=>'address_1','address2'=>'address_2','state'=>'province_code'])[$key] => $item ] :[ $key => $item ];
}),
$this->fields) );
}
public function render()
{
return render('gateways.rotessa.components.address',array_merge( $this->defaults, $this->attributes->getAttributes() ) );
}
}
// AmericanBankInfo Component
class AccountComponent extends Component
{
private $fields = [
'bank_account_type',
'routing_number',
'institution_number',
'transit_number',
'bank_name',
'country',
'account_number'
];
private $defaults = [
'bank_account_type' => null,
'routing_number' => null,
'institution_number' => null,
'transit_number' => null,
'bank_name' => ' ',
'account_number' => null,
'country' => 'US',
"authorization_type" => 'Online'
];
public array $account;
public function __construct(array $account) {
$this->account = $account;
$this->attributes = $this->newAttributeBag(Arr::only($this->account, $this->fields) );
}
public function render()
{
return render('gateways.rotessa.components.account', array_merge($this->attributes->getAttributes(), $this->defaults) );
}
}

View File

@ -0,0 +1,16 @@
<?php
use Illuminate\Support\Facades\View;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
View::composer(['*.rotessa.components.address','*.rotessa.components.banks.US.bank','*.rotessa.components.dropdowns.country.US'], function ($view) {
$states = USStates::get();
$view->with('states', $states);
});
// CAProvinces View Composer
View::composer(['*.rotessa.components.address','*.rotessa.components.banks.CA.bank','*.rotessa.components.dropdowns.country.CA'], function ($view) {
$provinces = CAProvinces::get();
$view->with('provinces', $provinces);
});

View File

@ -105,7 +105,9 @@ class Gateway extends StaticModel
$link = 'https://www.forte.net/';
} elseif ($this->id == 62) {
$link = 'https://docs.btcpayserver.org/InvoiceNinja/';
}
} elseif ($this->id == 4002) {
$link = 'https://rotessa.com';
}
return $link;
}
@ -224,6 +226,15 @@ class Gateway extends StaticModel
return [
GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']],
]; //BTCPay
case 4002:
return [
GatewayType::BANK_TRANSFER => [
'refund' => false,
'token_billing' => true,
'webhooks' => [],
],
GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []]
]; // Rotessa
default:
return [];
}

View File

@ -0,0 +1,215 @@
<?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\PaymentDrivers\Rotessa;
use Carbon\Carbon;
use App\Models\Client;
use App\Models\Payment;
use App\Models\SystemLog;
use Illuminate\View\View;
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Exceptions\PaymentFailed;
use App\DataProviders\Frequencies;
use App\Models\ClientGatewayToken;
use Illuminate\Http\RedirectResponse;
use App\PaymentDrivers\RotessaPaymentDriver;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\Resources\Transaction;
use Omnipay\Common\Exception\InvalidRequestException;
use Omnipay\Common\Exception\InvalidResponseException;
use App\Exceptions\Ninja\ClientPortalAuthorizationException;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class PaymentMethod implements MethodInterface
{
protected RotessaPaymentDriver $rotessa;
public function __construct(RotessaPaymentDriver $rotessa)
{
$this->rotessa = $rotessa;
$this->rotessa->init();
}
/**
* Show the authorization page for Rotessa.
*
* @param array $data
* @return \Illuminate\View\View
*/
public function authorizeView(array $data): View
{
$data['contact'] = collect($data['client']->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' => $data['client']->phone,
'custom_identifier' => $data['client']->number,
'name' => $data['client']->name,
'id' => null
] )->all();
$data['gateway'] = $this->rotessa;
$data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method'));
$data['account'] = [
'routing_number' => $data['client']->routing_id,
'country' => $data['client']->country->iso_3166_2
];
$data['address'] = collect($data['client']->toArray())->merge(['country' => $data['client']->country->iso_3166_2 ])->all();
return render('gateways.rotessa.bank_transfer.authorize', $data );
}
/**
* Handle the authorization page for Rotessa.
*
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
{
try {
$request->validate([
'gateway_type_id' => ['required','integer'],
'country' => ['required'],
'name' => ['required'],
'address_1' => ['required'],
'address_2' => ['required'],
'city' => ['required'],
'email' => ['required','email:filter'],
'province_code' => ['required','size:2','alpha'],
'postal_code' => ['required'],
'authorization_type' => ['required'],
'account_number' => ['required'],
'bank_name' => ['required'],
'phone' => ['required'],
'home_phone' => ['required'],
'bank_account_type'=>['required_if:country,US'],
'routing_number'=>['required_if:country,US'],
'institution_number'=>['required_if:country,CA','numeric'],
'transit_number'=>['required_if:country,CA','numeric'],
'custom_identifier'=>['required_without:customer_id'],
'customer_id'=>['required_without:custom_identifier','integer'],
]);
$customer = new Customer( ['address' => $request->only('address_1','address_2','city','postal_code','province_code','country'), 'custom_identifier' => $request->input('custom_identifier') ] + $request->all());
$this->rotessa->findOrCreateCustomer($customer->resolve());
return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added'));
} catch (\Throwable $e) {
return $this->rotessa->processInternallyFailedPayment($this->rotessa, new ClientPortalAuthorizationException( get_class( $e) . " : {$e->getMessage()}", (int) $e->getCode() ));
}
return back()->withMessage(ctrans('texts.unable_to_verify_payment_method'));
}
/**
* Payment view for the Rotessa.
*
* @param array $data
* @return \Illuminate\View\View
*/
public function paymentView(array $data): View
{
$data['gateway'] = $this->rotessa;
$data['amount'] = $data['total']['amount_with_fee'];
$data['due_date'] = date('Y-m-d', min(max(strtotime($data['invoices']->max('due_date')), strtotime('now')), strtotime('+1 day')));
$data['process_date'] = $data['due_date'];
$data['currency'] = $this->rotessa->client->getCurrencyCode();
$data['frequency'] = Frequencies::getOnePayment();
$data['installments'] = 1;
$data['invoice_nums'] = $data['invoices']->pluck('invoice_number')->join(', ');
return render('gateways.rotessa.bank_transfer.pay', $data );
}
/**
* Handle payments page for Rotessa.
*
* @param PaymentResponseRequest $request
* @return void
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$response= null;
$customer = null;
try {
$request->validate([
'source' => ['required','string','exists:client_gateway_tokens,token'],
'amount' => ['required','numeric'],
'token_id' => ['required','integer','exists:client_gateway_tokens,id'],
'process_date'=> ['required','date','after_or_equal:today'],
]);
$customer = ClientGatewayToken::query()
->where('company_gateway_id', $this->rotessa->company_gateway->id)
->where('client_id', $this->rotessa->client->id)
->where('id', (int) $request->input('token_id'))
->where('token', $request->input('source'))
->first();
if(!$customer) throw new \Exception('Client gateway token not found!', 605);
$transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date','comment'));
$transaction->additional(['customer_id' => $customer->gateway_customer_reference]);
$transaction = array_filter( $transaction->resolve());
$response = $this->rotessa->gateway->capture($transaction)->send();
if(!$response->isSuccessful()) throw new \Exception($response->getMessage(), (int) $response->getCode());
return $this->processPendingPayment($response->getParameter('id'), (float) $response->getParameter('amount'), (int) $customer->gateway_type_id , $customer->token);
} catch(\Throwable $e) {
$this->processUnsuccessfulPayment( new InvalidResponseException($e->getMessage(), (int) $e->getCode()) );
}
}
public function processPendingPayment($payment_id, float $amount, int $gateway_type_id, $payment_method )
{
$data = [
'payment_method' => $payment_method,
'payment_type' => $gateway_type_id,
'amount' => $amount,
'transaction_reference' =>$payment_id,
'gateway_type_id' => $gateway_type_id,
];
$payment = $this->rotessa->createPayment($data, Payment::STATUS_PENDING);
SystemLogger::dispatch(
[ 'data' => $data ],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
880,
$this->rotessa->client,
$this->rotessa->client->company,
);
return redirect()->route('client.payments.show', [ 'payment' => $this->rotessa->encodePrimaryKey($payment->id) ]);
}
/**
* Handle unsuccessful payment for Rotessa.
*
* @param Exception $exception
* @throws PaymentFailed
* @return void
*/
public function processUnsuccessfulPayment(\Exception $exception): void
{
$this->rotessa->sendFailureMail($exception->getMessage());
SystemLogger::dispatch(
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
880,
$this->rotessa->client,
$this->rotessa->client->company,
);
throw new PaymentFailed($exception->getMessage(), $exception->getCode());
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\PaymentDrivers\Rotessa\Resources;
use Illuminate\Http\Request;
use Omnipay\Rotessa\Model\CustomerModel;
use Illuminate\Http\Resources\Json\JsonResource;
class Customer extends JsonResource
{
function __construct($resource) {
parent::__construct( new CustomerModel($resource));
}
function jsonSerialize() : array {
return $this->resource->jsonSerialize();
}
function toArray(Request $request) : array {
return $this->additional + parent::toArray($request);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\PaymentDrivers\Rotessa\Resources;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Omnipay\Rotessa\Model\TransactionScheduleModel;
class Transaction extends JsonResource
{
function __construct($resource) {
parent::__construct( new TransactionScheduleModel( $resource));
}
function jsonSerialize() : array {
return $this->resource->jsonSerialize();
}
function toArray(Request $request) : array {
return $this->additional + parent::toArray($request);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Common\AbstractGateway;
use Omnipay\Rotessa\ClientInterface;
use Omnipay\Rotessa\Message\RequestInterface;
abstract class AbstractClient extends AbstractGateway implements ClientInterface
{
protected $default_parameters = [];
public function getDefaultParameters() : array {
return $this->default_parameters;
}
public function setDefaultParameters(array $params) {
$this->default_parameters = $params;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Rotessa\Message\Request\RequestInterface;
trait ApiTrait
{
public function getCustomers() : RequestInterface {
return $this->createRequest('GetCustomers', [] );
}
public function postCustomers(array $params) : RequestInterface {
return $this->createRequest('PostCustomers', $params );
}
public function getCustomersId(array $params) : RequestInterface {
return $this->createRequest('GetCustomersId', $params );
}
public function patchCustomersId(array $params) : RequestInterface {
return $this->createRequest('PatchCustomersId', $params );
}
public function postCustomersShowWithCustomIdentifier(array $params) : RequestInterface {
return $this->createRequest('PostCustomersShowWithCustomIdentifier', $params );
}
public function getTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('GetTransactionSchedulesId', $params );
}
public function deleteTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('DeleteTransactionSchedulesId', $params );
}
public function patchTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('PatchTransactionSchedulesId', $params );
}
public function postTransactionSchedules(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedules', $params );
}
public function postTransactionSchedulesCreateWithCustomIdentifier(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedulesCreateWithCustomIdentifier', $params );
}
public function postTransactionSchedulesUpdateViaPost(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedulesUpdateViaPost', $params );
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Common\GatewayInterface;
use Omnipay\Rotessa\Message\Request\RequestInterface;
interface ClientInterface extends GatewayInterface
{
public function getDefaultParameters(): array;
public function setDefaultParameters(array $data);
}

View File

@ -0,0 +1,43 @@
<?php
namespace Omnipay\Rotessa\Exception;
class BadRequestException extends \Exception {
protected $message = "Your request includes invalid parameters";
protected $code = 400;
}
class UnauthorizedException extends \Exception {
protected $message = "Your API key is not valid or is missing";
protected $code = 401;
}
class NotFoundException extends \Exception {
protected $message = "The specified resource could not be found";
protected $code = 404;
}
class NotAcceptableException extends \Exception {
protected $message = "You requested a format that isnt json";
protected $code = 406;
}
class UnprocessableEntityException extends \Exception {
protected $message = "Your request results in invalid data";
protected $code = 422;
}
class InternalServerErrorException extends \Exception {
protected $message = "We had a problem with our server. Try again later";
protected $code = 500;
}
class ServiceUnavailableException extends \Exception {
protected $message = "We're temporarily offline for maintenance. Please try again later";
protected $code = 503;
}
class ValidationException extends \Exception {
protected $message = "A validation error has occured";
protected $code = 600;
}

View File

@ -0,0 +1,74 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Rotessa\ApiTrait;
use Omnipay\Rotessa\AbstractClient;
use Omnipay\Rotessa\ClientInterface;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class Gateway extends AbstractClient implements ClientInterface {
use ApiTrait;
protected $default_parameters = ['api_key' => 1234567890 ];
protected $test_mode = true;
protected $api_key;
public function getName()
{
return 'Rotessa';
}
public function getDefaultParameters() : array
{
return array_merge($this->default_parameters, array('api_key' => $this->api_key, 'test_mode' => $this->test_mode ) );
}
public function setTestMode($value) {
$this->test_mode = $value;
}
public function getTestMode() {
return $this->test_mode;
}
protected function createRequest($class_name, ?array $parameters = [] ) :RequestInterface {
$class = null;
$class_name = "Omnipay\\Rotessa\\Message\\Request\\$class_name";
$parameters = $class_name::hasModel() ? (($parameters = ($class_name::getModel($parameters)))->validate() ? $parameters->jsonSerialize() : null ) : $parameters;
try {
$class = new $class_name($this->httpClient, $this->httpRequest, $this->getDefaultParameters() + $parameters );
} catch (\Throwable $th) {
throw $th;
}
return $class;
}
function setApiKey($value) {
$this->api_key = $value;
}
function getApiKey() {
return $this->api_key;
}
function authorize(array $options = []) : RequestInterface {
return $this->postCustomers($options);
}
function capture(array $options = []) : RequestInterface {
return array_key_exists('customer_id', $options)? $this->postTransactionSchedules($options) : $this->postTransactionSchedulesCreateWithCustomIdentifier($options) ;
}
function updateCustomer(array $options) : RequestInterface {
return $this->patchCustomersId($options);
}
function fetchTransaction($id = null) : RequestInterface {
return $this->getTransactionSchedulesId(compact('id'));
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Omnipay\Rotessa\Http;
use Omnipay\Common\Http\Client as HttpClient;
use Omnipay\Common\Http\Exception\NetworkException;
use Omnipay\Common\Http\Exception\RequestException;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class Client extends HttpClient
{
/**
* The Http Client which implements `public function sendRequest(RequestInterface $request)`
* Note: Will be changed to PSR-18 when released
*
* @var HttpClient
*/
private $httpClient;
/**
* @var RequestFactory
*/
private $requestFactory;
public function __construct($httpClient = null, RequestFactory $requestFactory = null)
{
$this->httpClient = $httpClient ?: HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find();
parent::__construct($httpClient, $requestFactory);
}
/**
* @param $method
* @param $uri
* @param array $headers
* @param string|array|resource|StreamInterface|null $body
* @param string $protocolVersion
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
public function request(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return $this->sendRequest($method, $uri, $headers, $body, $protocolVersion);
}
/**
* @param RequestInterface $request
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
private function sendRequest( $method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1')
{
$response = null;
try {
if( method_exists($this->httpClient, 'sendRequest'))
$response = $this->httpClient->sendRequest( $this->requestFactory->createRequest($method, $uri, $headers, $body, $protocolVersion));
else $response = $this->httpClient->request($method, $uri, compact('body','headers'));
} catch (\Http\Client\Exception\NetworkException $networkException) {
throw new NetworkException($networkException->getMessage(), $request, $networkException);
} catch (\Exception $exception) {
throw new RequestException($exception->getMessage(), $request, $exception);
}
return $response;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Omnipay\Rotessa\Http\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
class Response extends JsonResponse
{
protected $reason_phrase = '';
protected $reason_code = '';
public function __construct(mixed $data = null, int $status = 200, array $headers = [])
{
parent::__construct($data , $status, $headers, true);
if(array_key_exists('errors',$data = json_decode( $this->content, true) )) {
$data = $data['errors'][0];
$this->reason_phrase = $data['error_message'] ;
$this->reason_code = $data['error_message'] ;
}
}
public function getReasonPhrase() {
return $this->reason_phrase;
}
public function getReasonCode() {
return $this->reason_code;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Omnipay\Rotessa;
trait IsValidTypeTrait {
public static function isValid(string $value) {
return in_array($value, self::getTypes());
}
abstract public static function getTypes() : array;
}

View File

@ -0,0 +1,52 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Common\Message\AbstractRequest as Request;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
abstract class AbstractRequest extends Request implements RequestInterface
{
protected $test_mode = false;
protected $api_version;
protected $method = 'GET';
protected $endpoint;
protected $api_key;
public function setApiKey(string $value) {
$this->api_key = $value;
}
public function getData() {
try {
if(empty($this->api_key)) throw new \Exception('No Api Key Found!');
$this->validate( ...array_keys($data = $this->getParameters()));
} catch (\Throwable $th) {
throw new \Omnipay\Rotessa\Exception\ValidationException($th->getMessage() , 600, $th);
}
return (array) $data;
}
abstract public function sendData($data) : ResponseInterface;
abstract protected function sendRequest(string $method, string $endpoint, array $headers = [], array $data = [] );
abstract protected function createResponse(array $data) : ResponseInterface;
abstract public function getEndpointUrl(): string;
public function getEndpoint() : string {
return $this->endpoint;
}
public function getTestMode() {
return $this->test_mode;
}
public function setTestMode($mode) {
$this->test_mode = $mode;
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Common\Http\ClientInterface;
use Omnipay\Rotessa\Http\Response\Response;
use Omnipay\Rotessa\Message\Response\BaseResponse;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
class BaseRequest extends AbstractRequest implements RequestInterface
{
protected $base_url = 'rotessa.com';
protected $api_version = 1;
protected $endpoint = '';
const ENVIRONMENT_SANDBOX = 'sandbox-api';
const ENVIRONMENT_LIVE = 'api';
function __construct(ClientInterface $http_client = null, HttpRequest $http_request, $model ) {
parent::__construct($http_client, $http_request );
$this->initialize($model);
}
protected function sendRequest(string $method, string $endpoint, array $headers = [], array $data = [])
{
/**
* @param $method
* @param $uri
* @param array $headers
* @param string|resource|StreamInterface|null $body
* @param string $protocolVersion
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
$response = $this->httpClient->request($method, $endpoint, $headers, json_encode($data) ) ;
$this->response = new Response ($response->getBody()->getContents(), $response->getStatusCode(), $response->getHeaders(), true);
}
protected function createResponse(array $data): ResponseInterface {
return new BaseResponse($this, $data, $this->response->getStatusCode(), $this->response->getReasonPhrase());
}
protected function replacePlaceholder($string, $array) {
$pattern = "/\{([^}]+)\}/";
$replacement = function($matches) use($array) {
$key = $matches[1];
if (array_key_exists($key, $array)) {
return $array[$key];
} else {
return $matches[0];
}
};
return preg_replace_callback($pattern, $replacement, $string);
}
public function sendData($data) :ResponseInterface {
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => "Token token={$this->api_key}"
];
$this->sendRequest(
$this->method,
$this->getEndpointUrl(),
$headers,
$data);
return $this->createResponse(json_decode($this->response->getContent(), true));
}
public function getEndpoint() : string {
return $this->replacePlaceholder($this->endpoint, $this->getParameters());
}
public function getEndpointUrl() : string {
return sprintf('https://%s.%s/v%d%s',$this->test_mode ? self::ENVIRONMENT_SANDBOX : self::ENVIRONMENT_LIVE ,$this->base_url, $this->api_version, $this->getEndpoint());
}
public static function hasModel() : bool {
return (bool) static::$model;
}
public static function getModel($parameters = []) {
$class_name = static::$model;
$class_name = "Omnipay\\Rotessa\\Model\\{$class_name}Model";
return new $class_name($parameters);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class DeleteTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'DELETE';
protected static $model = '';
public function setId(string $value) {
$this->setParameter('id',$value);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetCustomers extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers';
protected $method = 'GET';
protected static $model = '';
}

View File

@ -0,0 +1,19 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetCustomersId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/{id}';
protected $method = 'GET';
protected static $model = '';
public function setId(int $value) {
$this->setParameter('id',$value);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'GET';
protected static $model = '';
public function setId(int $value) {
$this->setParameter('id',$value);
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PatchCustomersId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/{id}';
protected $method = 'PATCH';
protected static $model = 'CustomerPatch';
public function setId(string $value) {
$this->setParameter('id',$value);
}
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
public function setName(string $value) {
$this->setParameter('name',$value);
}
public function setEmail(string $value) {
$this->setParameter('email',$value);
}
public function setCustomerType(string $value) {
$this->setParameter('customer_type',$value);
}
public function setHomePhone(string $value) {
$this->setParameter('home_phone',$value);
}
public function setPhone(string $value) {
$this->setParameter('phone',$value);
}
public function setBankName(string $value) {
$this->setParameter('bank_name',$value);
}
public function setInstitutionNumber(string $value) {
$this->setParameter('institution_number',$value);
}
public function setTransitNumber(string $value) {
$this->setParameter('transit_number',$value);
}
public function setBankAccountType(string $value) {
$this->setParameter('bank_account_type',$value);
}
public function setAuthorizationType(string $value) {
$this->setParameter('authorization_type',$value);
}
public function setRoutingNumber(string $value) {
$this->setParameter('routing_number',$value);
}
public function setAccountNumber(string $value) {
$this->setParameter('account_number',$value);
}
public function setAddress(array $value) {
$this->setParameter('address',$value);
}
public function setTransactionSchedules(array $value) {
$this->setParameter('transaction_schedules',$value);
}
public function setFinancialTransactions(array $value) {
$this->setParameter('financial_transactions',$value);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PatchTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'PATCH';
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostCustomers extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers';
protected $method = 'POST';
protected static $model = 'Customer';
public function setId(string $value) {
$this->setParameter('id',$value);
}
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
public function setName(string $value) {
$this->setParameter('name',$value);
}
public function setEmail(string $value) {
$this->setParameter('email',$value);
}
public function setCustomerType(string $value) {
$this->setParameter('customer_type',$value);
}
public function setHomePhone(string $value) {
$this->setParameter('home_phone',$value);
}
public function setPhone(string $value) {
$this->setParameter('phone',$value);
}
public function setBankName(string $value) {
$this->setParameter('bank_name',$value);
}
public function setInstitutionNumber(string $value = '') {
$this->setParameter('institution_number',$value);
}
public function setTransitNumber(string $value = '') {
$this->setParameter('transit_number',$value);
}
public function setBankAccountType(string $value) {
$this->setParameter('bank_account_type',$value);
}
public function setAuthorizationType(string $value = '') {
$this->setParameter('authorization_type',$value);
}
public function setRoutingNumber(string $value = '') {
$this->setParameter('routing_number',$value);
}
public function setAccountNumber(string $value) {
$this->setParameter('account_number',$value);
}
public function setAddress(array $value) {
$this->setParameter('address',$value);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostCustomersShowWithCustomIdentifier extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/show_with_custom_identifier';
protected $method = 'POST';
protected static $model = null;
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedules extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules';
protected $method = 'POST';
protected static $model = 'TransactionSchedule';
public function setCustomerId(string $value) {
$this->setParameter('customer_id',$value);
}
public function setProcessDate(string $value) {
$this->setParameter('process_date',$value);
}
public function setFrequency(string $value) {
$this->setParameter('frequency',$value);
}
public function setInstallments(int $value) {
$this->setParameter('installments',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedulesCreateWithCustomIdentifier extends PostTransactionSchedules implements RequestInterface
{
protected $endpoint = '/transaction_schedules/create_with_custom_identifier';
protected $method = 'POST';
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedulesUpdateViaPost extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/update_via_post';
protected $method = 'POST';
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Omnipay\Common\Message\RequestInterface as MessageInterface;
interface RequestInterface extends MessageInterface
{
}

View File

@ -0,0 +1,16 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Common\Message\AbstractResponse as Response;
abstract class AbstractResponse extends Response implements ResponseInterface
{
abstract public function getData();
abstract public function getCode();
abstract public function getMessage();
abstract public function getParameter(string $key);
}

View File

@ -0,0 +1,44 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Omnipay\Common\Message\AbstractResponse as Response;
class BaseResponse extends Response implements ResponseInterface
{
protected $code = 0;
protected $message = null;
function __construct(RequestInterface $request, array $data = [], int $code = 200, string $message = null ) {
parent::__construct($request, $data);
$this->code = $code;
$this->message = $message;
}
public function getData() {
return $this->getParameters();
}
public function getCode() {
return (int) $this->code;
}
public function isSuccessful() {
return $this->code < 300;
}
public function getMessage() {
return $this->message;
}
protected function getParameters() {
return $this->data;
}
public function getParameter(string $key) {
return $this->getParameters()[$key];
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Common\Message\ResponseInterface as MessageInterface;
interface ResponseInterface extends MessageInterface
{
public function getParameter(string $key) ;
}

View File

@ -0,0 +1,63 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Common\ParametersTrait;
use Omnipay\Rotessa\Model\ModelInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Omnipay\Rotessa\Exception\ValidationException;
abstract class AbstractModel implements ModelInterface {
use ParametersTrait;
abstract public function jsonSerialize() : array;
public function validate() : bool {
$required = array_diff_key( array_flip($this->required), array_filter($this->getParameters()) );
if(!empty($required)) throw new ValidationException("Could not validate " . implode(",", array_keys($required)) );
return true;
}
public function __get($key) {
return array_key_exists($key, $this->attributes) ? $this->getParameter($key) : null;
}
public function __set($key, $value) {
if(array_key_exists($key, $this->attributes)) $this->setParameter($key, $value);
}
public function __toString() : string {
return json_encode($this);
}
public function toString() : string {
return $this->__toString();
}
public function __toArray() : array {
return $this->getParameters();
}
public function toArray() : array {
return $this->__toArray();
}
public function initialize(array $params = []) {
$this->parameters = new ParameterBag;
$parameters = array_merge($this->defaults, $params);
if ($parameters) {
foreach ($this->attributes as $param => $type) {
$value = @$parameters[$param];
if($value){
settype($value, $type);
$this->setParameter($param, $value);
}
}
}
return $this;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Omnipay\Rotessa\Model;
use \DateTime;
use Omnipay\Rotessa\Model\AbstractModel;
use Omnipay\Rotessa\Model\ModelInterface;
class BaseModel extends AbstractModel implements ModelInterface {
protected $attributes = [
"id" => "string"
];
protected $required = ['id'];
protected $defaults = ['id' => 0 ];
public function __construct($parameters = array()) {
$this->initialize($parameters);
}
public function jsonSerialize() : array {
return array_intersect_key($this->toArray(), array_flip($this->required) );
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Object\Country;
use Omnipay\Rotessa\Object\Address;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Object\CustomerType;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Object\BankAccountType;
use Omnipay\Rotessa\Object\AuthorizationType;
use Omnipay\Rotessa\Exception\ValidationException;
class CustomerModel extends BaseModel implements ModelInterface {
protected $attributes = [
"id" => "string",
"custom_identifier" => "string",
"name" => "string",
"email" => "string",
"customer_type" => "string",
"home_phone" => "string",
"phone" => "string",
"bank_name" => "string",
"institution_number" => "string",
"transit_number" => "string",
"bank_account_type" => "string",
"authorization_type" => "string",
"routing_number" => "string",
"account_number" => "string",
"address" => "object",
"transaction_schedules" => "array",
"financial_transactions" => "array",
"active" => "bool"
];
protected $defaults = ["active" => false,"customer_type" =>'Business',"bank_account_type" =>'Savings',"authorization_type" =>'Online',];
protected $required = ["name","email","customer_type","home_phone","phone","bank_name","institution_number","transit_number","bank_account_type","authorization_type","routing_number","account_number","address",'custom_identifier'];
public function validate() : bool {
try {
$country = $this->address->country;
if(!self::isValidCountry($country)) throw new \Exception("Invalid country!");
$this->required = array_diff($this->required, Country::isAmerican($country) ? ["institution_number", "transit_number"] : ["bank_account_type", "routing_number"]);
parent::validate();
if(Country::isCanadian($country) ) {
if(!self::isValidTransitNumber($this->getParameter('transit_number'))) throw new \Exception("Invalid transit number!");
if(!self::isValidInstitutionNumber($this->getParameter('institution_number'))) throw new \Exception("Invalid institution number!");
}
if(!self::isValidCustomerType($this->getParameter('customer_type'))) throw new \Exception("Invalid customer type!");
if(!self::isValidBankAccountType($this->getParameter('bank_account_type'))) throw new \Exception("Invalid bank account type!");
if(!self::isValidAuthorizationType($this->getParameter('authorization_type'))) throw new \Exception("Invalid authorization type!");
} catch (\Throwable $th) {
throw new ValidationException($th->getMessage());
}
return true;
}
public static function isValidCountry(string $country ) : bool {
return Country::isValidCountryCode($country) || Country::isValidCountryName($country);
}
public static function isValidTransitNumber(string $value ) : bool {
return strlen($value) == 5;
}
public static function isValidInstitutionNumber(string $value ) : bool {
return strlen($value) == 3;
}
public static function isValidCustomerType(string $value ) : bool {
return CustomerType::isValid($value);
}
public static function isValidBankAccountType(string $value ) : bool {
return BankAccountType::isValid($value);
}
public static function isValidAuthorizationType(string $value ) : bool {
return AuthorizationType::isValid($value);
}
public function toArray() : array {
return [ 'address' => (array) $this->getParameter('address') ] + parent::toArray();
}
public function jsonSerialize() : array {
$address = (array) $this->getParameter('address');
unset($address['country']);
return compact('address') + parent::jsonSerialize();
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Object\Country;
use Omnipay\Rotessa\Object\Address;
use Omnipay\Rotessa\Object\CustomerType;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Object\BankAccountType;
use Omnipay\Rotessa\Object\AuthorizationType;
use Omnipay\Rotessa\Exception\ValidationException;
class CustomerPatchModel extends CustomerModel implements ModelInterface {
protected $required = ["id","custom_identifier","name","email","customer_type","home_phone","phone","bank_name","institution_number","transit_number","bank_account_type","authorization_type","routing_number","account_number","address"];
}

View File

@ -0,0 +1,8 @@
<?php
namespace Omnipay\Rotessa\Model;
interface ModelInterface extends \JsonSerializable
{
public function __toArray();
public function __toString();
}

View File

@ -0,0 +1,84 @@
<?php
namespace Omnipay\Rotessa\Model;
use \DateTime;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Object\Frequency;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Exception\ValidationException;
class TransactionScheduleModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"id" => "string",
"amount" => "float",
"comment" => "string",
"created_at" => "date",
"financial_transactions" => "array",
"frequency" => "string",
"installments" => "integer",
"next_process_date" => "date",
"process_date" => "date",
"updated_at" => "date",
"customer_id" => "string",
"custom_identifier" => "string",
];
public const DATE_FORMAT = 'F j, Y';
protected $defaults = ["amount" =>0.00,"comment" =>' ',"financial_transactions" =>0,"frequency" =>'Once',"installments" =>1];
protected $required = ["amount","comment","frequency","installments","process_date"];
public function validate() : bool {
try {
parent::validate();
if(!self::isValidDate($this->process_date)) throw new \Exception("Could not validate date ");
if(!self::isValidFrequency($this->frequency)) throw new \Exception("Invalid frequency");
if(is_null($this->customer_id) && is_null($this->custom_identifier)) throw new \Exception("customer id or custom identifier is invalid");
} catch (\Throwable $th) {
throw new ValidationException($th->getMessage());
}
return true;
}
public function jsonSerialize() : array {
return ['customer_id' => $this->getParameter('customer_id'), 'custom_identifier' => $this->getParameter('custom_identifier') ] + parent::jsonSerialize() ;
}
public function __toArray() : array {
return parent::__toArray() ;
}
public function initialize(array $params = [] ) {
$o_params = array_intersect_key(
$params = array_intersect_key($params, $this->attributes),
($attr = array_filter($this->attributes, fn($p) => $p != "date"))
);
parent::initialize($o_params);
$d_params = array_diff_key($params, $attr);
array_walk($d_params, function($v,$k) {
$this->setParameter($k, self::formatDate( $v) );
}, );
return $this;
}
public static function isValidDate($date) : bool {
$d = DateTime::createFromFormat(self::DATE_FORMAT, $date);
// Check if the date is valid and matches the format
return $d && $d->format(self::DATE_FORMAT) === $date;
}
public static function isValidFrequency($value) : bool {
return Frequency::isValid($value);
}
protected static function formatDate($date) : string {
$d = new DateTime($date);
return $d->format(self::DATE_FORMAT);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Model\ModelInterface;
class TransactionSchedulesIdBodyModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"amount" => "int",
"comment" => "string",
];
public const DATE_FORMAT = 'Y-m-d H:i:s';
private $_is_error = false;
protected $defaults = ["amount" =>0,"comment" =>'0',];
protected $required = ["amount","comment",];
}

View File

@ -0,0 +1,24 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Model\ModelInterface;
class TransactionSchedulesUpdateViaPostBodyModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"id" => "int",
"amount" => "int",
"comment" => "string",
];
public const DATE_FORMAT = 'Y-m-d H:i:s';
private $_is_error = false;
protected $defaults = ["amount" =>0,"comment" =>'0',];
protected $required = ["amount","comment",];
}

View File

@ -0,0 +1,53 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Common\ParametersTrait;
final class Address implements \JsonSerializable {
use ParametersTrait;
protected $attributes = [
"address_1" => "string",
"address_2" => "string",
"city" => "string",
"id" => "int",
"postal_code" => "string",
"province_code" => "string",
"country" => "string"
];
protected $required = ["address_1","address_2","city","postal_code","province_code",];
public function jsonSerialize() {
return array_intersect_key($this->getParameters(), array_flip($this->required));
}
public function getCountry() : string {
return $this->getParameter('country');
}
public function initialize(array $parameters) {
foreach($this->attributes as $param => $type) {
$value = @$parameters[$param] ;
settype($value, $type);
$value = $value ?? null;
$this->parameters->set($param, $value);
}
}
public function __toArray() : array {
return $this->getParameters();
}
public function __toString() : string {
return $this->getFullAddress();
}
public function getFullAddress() :string {
$full_address = $this->getParameters();
extract($full_address);
return "$address_1 $address_2, $city, $postal_code $province_code, $country";
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class AuthorizationType {
use isValidTypeTrait;
const IN_PERSON = "In Person";
const ONLINE = "Online";
public static function isInPerson($value) {
return $value === self::IN_PERSON;
}
public static function isOnline($value) {
return $value === self::ONLINE;
}
public static function getTypes() : array {
return [
self::IN_PERSON,
self::ONLINE
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class BankAccountType {
use IsValidTypeTrait;
const SAVINGS = "Savings";
const CHECKING = "Checking";
public static function isSavings($value) {
return $value === self::SAVINGS;
}
public static function isChecking($value) {
return $value === self::Checking;
}
public static function getTypes() : array {
return [
self::SAVINGS,
self::CHECKING
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class Country {
use IsValidTypeTrait;
protected static $codes = ['CA','US'];
protected static $names = ['United States', 'Canada'];
public static function isValidCountryName(string $value) {
return in_array($value, self::$names);
}
public static function isValidCountryCode(string $value) {
return in_array($value, self::$codes);
}
public static function isAmerican(string $value) : bool {
return $value == 'US' || $value == 'United States';
}
public static function isCanadian(string $value) : bool {
return $value == 'CA' || $value == 'Canada';
}
public static function getTypes() : array {
return $codes + $names;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class CustomerType {
use IsValidTypeTrait;
const PERSONAL = "Personal";
const BUSINESS = "Business";
public static function isPersonal($value) {
return $value === self::PERSONAL;
}
public static function isBusiness($value) {
return $value === self::BUSINESS;
}
public static function getTypes() : array {
return [
self::PERSONAL,
self::BUSINESS
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class Frequency {
use IsValidTypeTrait;
const ONCE = "Once";
const WEEKLY = "Weekly";
const OTHER_WEEK = "Every Other Week";
const MONTHLY= "Monthly";
const OTHER_MONTH = "Every Other Month";
const QUARTERLY = "Quarterly";
const SEMI_ANNUALLY = "Semi-Annually";
const YEARLY = "Yearly";
public static function isOnce($value) {
return $value === self::ONCE;
}
public static function isWeekly($value) {
return $value === self::WEEKLY;
}
public static function isOtherWeek($value) {
return $value === self::OTHER_WEEK;
}
public static function isMonthly($value) {
return $value === self::MONTHLY;
}
public static function isOtherMonth($value) {
return $value === self::OTHER_MONTH;
}
public static function isQuarterly($value) {
return $value === self::QUARTERLY;
}
public static function isSemiAnnually($value) {
return $value === self::SEMI_ANNUALLY;
}
public static function isYearly($value) {
return $value === self::YEARLY;
}
public static function getTypes() : array {
return [
self::ONCE,
self::WEEKLY,
self::OTHER_WEEK,
self::MONTHLY,
self::OTHER_MONTH,
self::QUARTERLY,
self::SEMI_ANNUALLY,
self::YEARLY
];
}
}

View File

@ -0,0 +1,206 @@
<?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\PaymentDrivers;
use Omnipay\Omnipay;
use App\Models\Client;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\PaymentHash;
use Illuminate\Support\Arr;
use App\Models\GatewayType;
use Omnipay\Rotessa\Gateway;
use App\Models\ClientContact;
use App\Utils\Traits\MakesHash;
use App\Jobs\Util\SystemLogger;
use App\PaymentDrivers\BaseDriver;
use App\Models\ClientGatewayToken;
use Illuminate\Database\Eloquent\Builder;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\PaymentMethod as Acss;
use App\PaymentDrivers\Rotessa\PaymentMethod as BankTransfer;
class RotessaPaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = false;
public $token_billing = true;
public $can_authorise_credit_card = true;
public Gateway $gateway;
public $payment_method;
public static $methods = [
GatewayType::BANK_TRANSFER => BankTransfer::class,
//GatewayType::BACS => Bacs::class,
GatewayType::ACSS => Acss::class,
// GatewayType::DIRECT_DEBIT => DirectDebit::class
];
public function init(): self
{
$this->gateway = Omnipay::create(
$this->company_gateway->gateway->provider
);
$this->gateway->initialize((array) $this->company_gateway->getConfig());
return $this;
}
public function gatewayTypes(): array
{
$types = [];
if ($this->client
&& $this->client->currency()
&& in_array($this->client->currency()->code, ['USD'])
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_2, ['US'])) {
$types[] = GatewayType::BANK_TRANSFER;
}
if ($this->client
&& $this->client->currency()
&& in_array($this->client->currency()->code, ['CAD'])
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_2, ['CA'])) {
$types[] = GatewayType::ACSS;
}
return $types;
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data);
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}
public function importCustomers() {
$this->init();
try {
$result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customers = collect($result->getData())->unique('email');
$client_emails = $customers->pluck('email')->all();
$company_id = $this->company_gateway->company->id;
$client_contacts = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get();
$client_contacts = $client_contacts->map(function($item, $key) use ($customers) {
return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number ]);
} );
$client_contacts->each(
function($contact) use ($customers) {
sleep(10);
$result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
$this->client = Client::find($contact->custom_identifier);
$customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
}
);
} catch (\Throwable $th) {
$data = [
'transaction_reference' => null,
'transaction_response' => $th->getMessage(),
'success' => false,
'description' => $th->getMessage(),
'code' =>(int) $th->getCode()
];
SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client , $this->company_gateway->company);
throw $th;
}
return true;
}
public function findOrCreateCustomer(array $data)
{
$result = null;
try {
$existing = ClientGatewayToken::query()
->where('company_gateway_id', $this->company_gateway->id)
->where('client_id', $this->client->id)
->orWhere(function (Builder $query) use ($data) {
$query->where('token', encrypt(join(".", Arr::only($data, 'id','custom_identifier'))) )
->where('gateway_customer_reference', Arr::only($data,'id'));
})
->exists();
if ($existing) return true;
else if(!Arr::has($data,'id')) {
$result = $this->gateway->authorize($data)->send();
if (!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customer = new Customer($result->getData());
$data = array_filter($customer->resolve());
}
$payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS;
$gateway_token = $this->storeGatewayToken( [
'payment_meta' => $data + ['brand' => 'Rotessa'],
'token' => encrypt(join(".", Arr::only($data, 'id','custom_identifier'))),
'payment_method_id' => $payment_method_id ,
], ['gateway_customer_reference' =>
$data['id']
, 'routing_number' => Arr::has($data,'routing_number') ? $data['routing_number'] : $data['transit_number'] ]);
return $data['id'];
throw new \Exception($result->getMessage(), (int) $result->getCode());
} catch (\Throwable $th) {
$data = [
'transaction_reference' => null,
'transaction_response' => $th->getMessage(),
'success' => false,
'description' => $th->getMessage(),
'code' =>(int) $th->getCode()
];
SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getData(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->client->company);
throw $th;
}
}
}

View File

@ -13,6 +13,7 @@ namespace App\Providers;
use App\Http\ViewComposers\PortalComposer;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Blade;
class ComposerServiceProvider extends ServiceProvider
{
@ -24,6 +25,9 @@ class ComposerServiceProvider extends ServiceProvider
public function boot()
{
view()->composer('portal.*', PortalComposer::class);
include_once app_path('Http/ViewComposers/RotessaComposer.php');
include_once app_path("Http/ViewComposers/Components/RotessaComponents.php");
Blade::componentNamespace('App\\Http\\ViewComposers\\Components', 'rotessa');
}
/**
@ -34,5 +38,6 @@ class ComposerServiceProvider extends ServiceProvider
public function register()
{
//
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider as BaseProvider;
class RotessaServiceProvider extends BaseProvider
{
protected string $moduleName = 'Rotessa';
protected string $moduleNameLower = 'rotessa';
/**
* Boot the application events.
*/
public function boot(): void
{
include_once app_path('Http/ViewComposers/RotessaComposer.php');
$this->registerComponent();
}
/**
* Register views.
*/
public function registerComponent(): void
{
Blade::componentNamespace('App\\Http\\ViewComposers\\Components', $this->moduleNameLower);
}
}

View File

@ -131,7 +131,8 @@
"app/Helpers/TranslationHelper.php",
"app/Helpers/Generic.php",
"app/Helpers/ClientPortal.php"
]
],
"classmap": ["app/PaymentDrivers/Rotessa/vendor/karneaud/omnipay-rotessa/src/Omnipay/Rotessa/"]
},
"autoload-dev": {
"psr-4": {

View File

@ -200,7 +200,7 @@ return [
App\Providers\MultiDBProvider::class,
App\Providers\ClientPortalServiceProvider::class,
App\Providers\NinjaTranslationServiceProvider::class,
App\Providers\StaticServiceProvider::class,
App\Providers\StaticServiceProvider::class
],
/*

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Models\Gateway;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Model::unguard();
\DB::statement('SET FOREIGN_KEY_CHECKS=0;');
$record = Gateway::where('name', '=', 'Rotessa')->first();
$count = (int) Gateway::count();
$configuration = new \stdClass;
$configuration->api_key = '';
$configuration->test_mode = true;
if (!$record) {
$gateway = new Gateway;
} else {
$gateway = $record;
}
$gateway->id = 4002;
$gateway->name = 'Rotessa';
$gateway->key = Str::lower(Str::random(32));
$gateway->provider = 'Rotessa';
$gateway->is_offsite = true;
$gateway->fields = \json_encode($configuration);
$gateway->visible = 1;
$gateway->site_url = "https://rotessa.com";
$gateway->default_gateway_type_id = 2;
$gateway->save();
Gateway::query()->where('name','=', 'Rotessa')->update(['visible' => 1]);
\DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Gateway::where('name', '=', 'Rotessa')->delete();
}
};

View File

@ -0,0 +1,4 @@
<dd> Gateway: </dd>
<dt>{{ $brand }}</dt>
<dd> Account Number: </dd>
<dt>{{ $account_number }}</dt>

View File

@ -0,0 +1,35 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => $gateway->company_gateway->label, 'card_title' =>\App\Models\GatewayType::getAlias($gateway_type_id )])
@section('gateway_content')
@if(session()->has('ach_error'))
<div class="alert alert-failure mb-4">
<p>{{ session('ach_error') }}</p>
</div>
@endif
<form action="{{ route('client.payment_methods.store', ['method' => $gateway_type_id ]) }}"
method="post" id="server_response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="gateway_type_id" value="{{ $gateway_type_id }}">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="is_default" id="is_default">
<x-rotessa::contact-component :contact="$contact"></x-rotessa::contact-component>
<x-rotessa::address-component :address="$address"></x-rotessa::address-component>
<x-rotessa::account-component :account="$account"></x-rotessa::account-component>
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-bank-account', 'type' => 'submit'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@endsection
@section('gateway_footer')
@endsection

View File

@ -0,0 +1,91 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Direct Debit', 'card_title' => 'Direct Debit'])
@section('gateway_content')
@if (count($tokens) > 0)
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="source" value="">
<input type="hidden" name="amount" value="{{ $amount }}">
<input type="hidden" name="currency" value="{{ $currency }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="token_id" value="">
<input type="hidden" name="frequency" value="Once">
<input type="hidden" name="installments" value="1">
<input type="hidden" name="comment" value="Payment for invoice # {{ $invoice_nums }}">
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if (count($tokens) > 0)
@foreach ($tokens as $token)
<label class="mr-4">
<input type="radio" data-token="{{ $token->token }}" data-token_id="{{ $token->id }}" name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token" />
<span class="ml-1 cursor-pointer">
{{ App\Models\GatewayType::getAlias($token->gateway_type_id) }} ({{ $token->meta->brand }})
&nbsp; Acc#: {{ $token->meta->account_number }}
</span>
</label><br/>
@endforeach
@endisset
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Process Date
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input autocomplete="new-password" readonly type="date" min="{{ $due_date }}" name="process_date" id="process_date" required class="input w-full" placeholder="" value="{{ old('process_date', $process_date ) }}">
</dd>
{{--
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Insallments
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="installments" name="installments" type="number" placeholder="Installments" required value="{{ old('installments',$installments) }}">
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Frequency
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="frequency" name="frequency" type="text" placeholder="Once/Weekly/Monthly/Annually" required >
</dd>
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Comments
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<textarea autocomplete="new-password" id="comment" name="comment" type="text" class="w-full py-2 px-3 rounded text-sm disabled:opacity-75 disabled:cursor-not-allowed undefined border border-gray-300" placeholder="" rows="5" style="background-color: rgb(255, 255, 255); border-color: rgb(209, 213, 219); color: rgb(42, 48, 61);"> </textarea>
</dd> --}}
@endcomponent
</form>
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Direct Debit', 'show_title' => false])
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="button button-link text-primary"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@endcomponent
@endif
@if (count($tokens) > 0)
@include('portal.ninja2020.gateways.includes.pay_now')
@endif
@endsection
@push('footer')
<script>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
document.querySelector('input[name=token_id]').value = element.target.dataset.token_id;
}));
document.getElementById('pay-now').addEventListener('click', function() {
document.getElementById('server-response').submit();
});
</script>
@endpush

View File

@ -0,0 +1,31 @@
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">
Account Information
</h3>
<p class="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
Enter the information for the bank account
</p>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Bank Name
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="bank_name" name="bank_name" type="text" placeholder="Bank Name" required value="{{ old('bank_name', $bank_name) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Account Number
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="account_number" name="account_number" type="text" placeholder="Account Number" required value="{{ old('account_number', $account_number) }}">
</dd>
</div>
<input type="hidden" name="authorization_type" id="authorization_type" value="{{ old('authorization_type',$authorization_type) }}" >
@include("portal.ninja2020.gateways.rotessa.components.banks.$country.bank", compact('bank_account_type','routing_number','institution_number','transit_number'))

View File

@ -0,0 +1,62 @@
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">
Address Information
</h3>
<p class="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
Enter the address information for the account holder
</p>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Address Line 1
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="address_1" name="address_1" type="text" placeholder="Address Line 1" required value="{{ old('address_1', $address_1) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Address Line 2
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="address_2" name="address_2" type="text" placeholder="Address Line 2" required value="{{ old('address_2', $address_2) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
City
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="city" name="city" type="text" placeholder="City" required value="{{ old('city', $city) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Postal Code
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="postal_code" name="postal_code" type="text" placeholder="Postal Code" required value="{{ old('postal_code', $postal_code ) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Country
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
@if('US' == $country)
<input type="radio" id="us" name="country" value="US" required @checked(old('country', $country) == 'US')>
<label for="us">United States</label><br>
@else
<input type="radio" id="ca" name="country" value="CA" required @checked(old('country', $country) == 'CA')>
<label for="ca">Canada</label><br>
@endif
</dd>
</div>
@include("portal.ninja2020.gateways.rotessa.components.dropdowns.country.$country",compact('province_code'))

View File

@ -0,0 +1,17 @@
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Transit Number
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="transit_number" max="5" name="transit_number" type="text" placeholder="Transit Number" required value="{{ old('transit_number',$transit_number) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Institution Number
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="institution_number" max="3" name="institution_number" type="text" placeholder="Institution Number" required value="{{ old('institution_number',$institution_number) }}">
</dd>
</div>

View File

@ -0,0 +1,28 @@
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Routing Number
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="routing_number" name="routing_number" type="text" placeholder="Routing Number" required value="{{ old('routing_number',$routing_number) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Account Type
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div class="sm:grid-cols-2 sm:flex">
<div class="flex items-center px-2">
<input id="bank_account_type_savings" name="bank_account_type" value="Savings" required @checked(old('bank_account_type', $bank_account_type)) type="radio" class="focus:ring-gray-500 h-4 w-4 border-gray-300 disabled:opacity-75 disabled:cursor-not-allowed">
<label for="bank_account_type_savings" class="ml-3 block text-sm font-medium cursor-pointer">Savings</label>
</div>
<div class="flex items-center px-2">
<input id="bank_account_type_checking" name="bank_account_type" value="Checking" required @checked(old('bank_account_type', $bank_account_type)) type="radio" class="focus:ring-gray-500 h-4 w-4 border-gray-300 disabled:opacity-75 disabled:cursor-not-allowed">
<label for="bank_account_type_checking" class="ml-3 block text-sm font-medium cursor-pointer">Checking</label>
</div>
</div>
</dd>
</div>

View File

@ -0,0 +1,69 @@
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">
Account Holder Information
</h3>
<p class="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
Enter the information for the account holder
</p>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Full Name
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="name" name="name" type="text" placeholder="Full Name" required value="{{ old('name',$name) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Email Address
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" name="email" id="email" type="email" placeholder="Email Address" required value="{{ old('email',$email) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Home Phone
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="home_phone" name="home_phone" type="text" placeholder="Home Phone" required value="{{ old('phone',$phone) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Other Phone
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="phone" name="phone" type="text" placeholder="Phone" required value="{{ old('phone',$phone) }}">
</dd>
</div>
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Customer Type
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<div class="sm:grid-cols-2 sm:flex">
<div class="flex items-center px-2">
<input id="customer_type_personal" name="customer_type" value="Personal" required @checked(old('customer_type', $customer_type) == 'Personal') type="radio" class="focus:ring-gray-500 h-4 w-4 border-gray-300 disabled:opacity-75 disabled:cursor-not-allowed">
<label for="customer_type_personal" class="ml-3 block text-sm font-medium cursor-pointer">Personal</label>
</div>
<div class="flex items-center px-2">
<input id="customer_type_business" name="customer_type" value="Business" required @checked(old('customer_type', $customer_type) == 'Business') type="radio" class="focus:ring-gray-500 h-4 w-4 border-gray-300 disabled:opacity-75 disabled:cursor-not-allowed">
<label for="customer_type_business" class="ml-3 block text-sm font-medium cursor-pointer">Business</label>
</div>
</div>
</dd>
</div>
<input name="id" type="hidden" value="{{ old('id', $id ) }}">
<input name="custom_identifier" type="hidden" value="{{ old('custom_identifer', $contact['custom_identifier']) }}">

View File

@ -0,0 +1,12 @@
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Province Code
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<select class="input w-full" id="province_code" name="province_code" required>
@foreach($provinces as $code => $province)
<option value="{{ $code }}" @selected(old('province_code', $province_code) == $code ) >{{ $province }}</option>
@endforeach
</select>
</dd>
</div>

View File

@ -0,0 +1,12 @@
<div class="px-4 py-2 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
State
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<select class="input w-full" id="province_code" required name="province_code">
@foreach($states as $code => $state)
<option value="{{ $code }}" @selected(old('province_code', $province_code) == $code ) >{{ $state }}</option>
@endforeach
</select>
</dd>
</div>