1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 12:42:36 +01:00

Support importing Stripe customer cards

This commit is contained in:
Hillel Coren 2017-08-31 15:55:15 +03:00
parent 63cddfe262
commit c68825d8a6
20 changed files with 325 additions and 44 deletions

View File

@ -38,6 +38,7 @@ if (! defined('APP_NAME')) {
define('ENTITY_EXPENSE_CATEGORY', 'expense_category');
define('ENTITY_PROJECT', 'project');
define('ENTITY_RECURRING_EXPENSE', 'recurring_expense');
define('ENTITY_CUSTOMER', 'customer');
define('INVOICE_TYPE_STANDARD', 1);
define('INVOICE_TYPE_QUOTE', 2);
@ -169,6 +170,7 @@ if (! defined('APP_NAME')) {
define('IMPORT_INVOICEABLE', 'Invoiceable');
define('IMPORT_INVOICEPLANE', 'InvoicePlane');
define('IMPORT_HARVEST', 'Harvest');
define('IMPORT_STRIPE', 'Stripe');
define('MAX_NUM_CLIENTS', 100);
define('MAX_NUM_CLIENTS_PRO', 20000);

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
class CreateCustomerRequest extends CustomerRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', ENTITY_CUSTOMER);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [
'token' => 'required',
'client_id' => 'required',
'contact_id' => 'required',
'payment_method.source_reference' => 'required',
];
return $rules;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Requests;
class CustomerRequest extends EntityRequest
{
protected $entityType = ENTITY_CUSTOMER;
}

View File

@ -25,6 +25,16 @@ class AccountGatewayToken extends Eloquent
*/
protected $casts = [];
/**
* @var array
*/
protected $fillable = [
'contact_id',
'account_gateway_id',
'client_id',
'token',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@ -41,6 +51,14 @@ class AccountGatewayToken extends Eloquent
return $this->belongsTo('App\Models\AccountGateway');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function contact()
{
return $this->belongsTo('App\Models\Contact');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
@ -49,6 +67,14 @@ class AccountGatewayToken extends Eloquent
return $this->hasOne('App\Models\PaymentMethod', 'id', 'default_payment_method_id');
}
/**
* @return mixed
*/
public function getEntityType()
{
return ENTITY_CUSTOMER;
}
/**
* @return mixed
*/

View File

@ -94,16 +94,16 @@ class Client extends EntityModel
{
return [
'first' => 'contact_first_name',
'last' => 'contact_last_name',
'last^last4' => 'contact_last_name',
'email' => 'contact_email',
'work|office' => 'work_phone',
'mobile|phone' => 'contact_phone',
'name|organization' => 'name',
'apt|street2|address2' => 'address2',
'street|address|address1' => 'address1',
'name|organization|description^card' => 'name',
'apt|street2|address2|line2' => 'address2',
'street|address1|line1^avs' => 'address1',
'city' => 'city',
'state|province' => 'state',
'zip|postal|code' => 'postal_code',
'zip|postal|code^avs' => 'postal_code',
'country' => 'country',
'public' => 'public_notes',
'private|note' => 'private_notes',

View File

@ -21,11 +21,26 @@ class PaymentMethod extends EntityModel
* @var array
*/
protected $dates = ['deleted_at'];
/**
* @var array
*/
protected $hidden = ['id'];
/**
* @var array
*/
protected $fillable = [
'contact_id',
'payment_type_id',
'source_reference',
'last4',
'expiration',
'email',
'currency_id',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/

View File

@ -21,4 +21,37 @@ class PaymentType extends Eloquent
{
return $this->belongsTo('App\Models\GatewayType');
}
public static function resolveAlias($cardName)
{
$cardTypes = [
'visa' => PAYMENT_TYPE_VISA,
'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS,
'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS,
'mastercard' => PAYMENT_TYPE_MASTERCARD,
'discover' => PAYMENT_TYPE_DISCOVER,
'jcb' => PAYMENT_TYPE_JCB,
'dinersclub' => PAYMENT_TYPE_DINERS,
'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE,
'chinaunionpay' => PAYMENT_TYPE_UNIONPAY,
'unionpay' => PAYMENT_TYPE_UNIONPAY,
'laser' => PAYMENT_TYPE_LASER,
'maestro' => PAYMENT_TYPE_MAESTRO,
'solo' => PAYMENT_TYPE_SOLO,
'switch' => PAYMENT_TYPE_SWITCH,
];
$cardName = strtolower(str_replace([' ', '-', '_'], '', $cardName));
if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) {
// Some gateways return extra stuff after the card name
$cardName = $matches[1];
}
if (! empty($cardTypes[$cardName])) {
return $cardTypes[$cardName];
} else {
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
}
}
}

View File

@ -114,6 +114,38 @@ class BaseTransformer extends TransformerAbstract
return $product->$field ?: $default;
}
/**
* @param $name
*
* @return null
*/
public function getContact($email)
{
$email = trim(strtolower($email));
if (! isset($this->maps['contact'][$email])) {
return false;
}
return $this->maps['contact'][$email];
}
/**
* @param $name
*
* @return null
*/
public function getCustomer($key)
{
$key = trim($key);
if (! isset($this->maps['customer'][$key])) {
return false;
}
return $this->maps['customer'][$key];
}
/**
* @param $name
*

View File

@ -0,0 +1,54 @@
<?php
namespace App\Ninja\Import\Stripe;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
use App\Models\PaymentType;
/**
* Class InvoiceTransformer.
*/
class CustomerTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return bool|Item
*/
public function transform($data)
{
if (! $contact = $this->getContact($data->email)) {
return false;
}
$account = auth()->user()->account;
$accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE);
if (! $accountGateway) {
return false;
}
if ($this->getCustomer($data->id) || $this->getCustomer($data->email)) {
return false;
}
return new Item($data, function ($data) use ($account, $contact, $accountGateway) {
return [
'contact_id' => $contact->id,
'client_id' => $contact->client_id,
'account_gateway_id' => $accountGateway->id,
'token' => $data->id,
'payment_method' => [
'contact_id' => $contact->id,
'payment_type_id' => PaymentType::resolveAlias($data->card_brand),
'source_reference' => $data->card_id,
'last4' => $data->card_last4,
'expiration' => $data->card_exp_year . '-' . $data->card_exp_month . '-01',
'email' => $contact->email,
'currency_id' => $account->getCurrencyId(),
]
];
});
}
}

View File

@ -994,39 +994,6 @@ class BasePaymentDriver
return $url;
}
protected function parseCardType($cardName)
{
$cardTypes = [
'visa' => PAYMENT_TYPE_VISA,
'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS,
'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS,
'mastercard' => PAYMENT_TYPE_MASTERCARD,
'discover' => PAYMENT_TYPE_DISCOVER,
'jcb' => PAYMENT_TYPE_JCB,
'dinersclub' => PAYMENT_TYPE_DINERS,
'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE,
'chinaunionpay' => PAYMENT_TYPE_UNIONPAY,
'unionpay' => PAYMENT_TYPE_UNIONPAY,
'laser' => PAYMENT_TYPE_LASER,
'maestro' => PAYMENT_TYPE_MAESTRO,
'solo' => PAYMENT_TYPE_SOLO,
'switch' => PAYMENT_TYPE_SWITCH,
];
$cardName = strtolower(str_replace([' ', '-', '_'], '', $cardName));
if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) {
// Some gateways return extra stuff after the card name
$cardName = $matches[1];
}
if (! empty($cardTypes[$cardName])) {
return $cardTypes[$cardName];
} else {
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
}
}
public function handleWebHook($input)
{
throw new Exception('Unsupported gateway');

View File

@ -7,6 +7,7 @@ use Exception;
use Session;
use Utils;
use App\Models\GatewayType;
use App\Models\PaymentType;
class BraintreePaymentDriver extends BasePaymentDriver
{
@ -158,7 +159,7 @@ class BraintreePaymentDriver extends BasePaymentDriver
$paymentMethod->source_reference = $response->token;
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) {
$paymentMethod->payment_type_id = $this->parseCardType($response->cardType);
$paymentMethod->payment_type_id = PaymentType::parseCardType($response->cardType);
$paymentMethod->last4 = $response->last4;
$paymentMethod->expiration = $response->expirationYear . '-' . $response->expirationMonth . '-01';
} elseif ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) {

View File

@ -6,6 +6,7 @@ use App\Models\Payment;
use App\Models\PaymentMethod;
use Cache;
use Exception;
use App\Models\PaymentType;
class StripePaymentDriver extends BasePaymentDriver
{
@ -189,7 +190,7 @@ class StripePaymentDriver extends BasePaymentDriver
// In that case we'd use GATEWAY_TYPE_TOKEN even though we're creating the credit card
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD) || $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01';
$paymentMethod->payment_type_id = $this->parseCardType($source['brand']);
$paymentMethod->payment_type_id = PaymentType::parseCardType($source['brand']);
} elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
$paymentMethod->routing_number = $source['routing_number'];
$paymentMethod->payment_type_id = PAYMENT_TYPE_ACH;

View File

@ -7,6 +7,7 @@ use App\Models\PaymentMethod;
use Exception;
use Session;
use Utils;
use App\Models\PaymentType;
class WePayPaymentDriver extends BasePaymentDriver
{
@ -159,7 +160,7 @@ class WePayPaymentDriver extends BasePaymentDriver
}
} else {
$paymentMethod->last4 = $source->last_four;
$paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name);
$paymentMethod->payment_type_id = PaymentType::parseCardType($source->credit_card_name);
$paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-01';
$paymentMethod->source_reference = $source->credit_card_id;
}

View File

@ -6,6 +6,13 @@ use App\Models\Contact;
class ContactRepository extends BaseRepository
{
public function all()
{
return Contact::scope()
->withTrashed()
->get();
}
public function save($data, $contact = false)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;

View File

@ -0,0 +1,42 @@
<?php
namespace App\Ninja\Repositories;
use App\Models\PaymentMethod;
use App\Models\AccountGatewayToken;
use DB;
class CustomerRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\AccountGatewayToken';
}
public function all()
{
return AccountGatewayToken::whereAccountId(auth()->user()->account_id)
->with(['contact'])
->get();
}
public function save($data)
{
$account = auth()->user()->account;
$customer = new AccountGatewayToken();
$customer->account_id = $account->id;
$customer->fill($data);
$customer->save();
$paymentMethod = PaymentMethod::createNew();
$paymentMethod->account_gateway_token_id = $customer->id;
$paymentMethod->fill($data['payment_method']);
$paymentMethod->save();
$customer->default_payment_method_id = $paymentMethod->id;
$customer->save();
return $customer;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Policies;
class CustomerPolicy extends EntityPolicy
{
}

View File

@ -31,6 +31,7 @@ class AuthServiceProvider extends ServiceProvider
\App\Models\BankAccount::class => \App\Policies\BankAccountPolicy::class,
\App\Models\PaymentTerm::class => \App\Policies\PaymentTermPolicy::class,
\App\Models\Project::class => \App\Policies\ProjectPolicy::class,
\App\Models\AccountGatewayToken::class => \App\Policies\CustomerPolicy::class,
];
/**

View File

@ -3,6 +3,7 @@
namespace App\Services;
use App\Models\Client;
use App\Models\Contact;
use App\Models\EntityModel;
use App\Models\Expense;
use App\Models\ExpenseCategory;
@ -10,8 +11,10 @@ use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Models\Vendor;
use App\Models\AccountGatewayToken;
use App\Ninja\Import\BaseTransformer;
use App\Ninja\Repositories\ClientRepository;
use App\Ninja\Repositories\CustomerRepository;
use App\Ninja\Repositories\ContactRepository;
use App\Ninja\Repositories\ExpenseCategoryRepository;
use App\Ninja\Repositories\ExpenseRepository;
@ -53,6 +56,11 @@ class ImportService
*/
protected $clientRepo;
/**
* @var CustomerRepository
*/
protected $customerRepo;
/**
* @var ContactRepository
*/
@ -90,6 +98,7 @@ class ImportService
ENTITY_TASK,
ENTITY_PRODUCT,
ENTITY_EXPENSE,
ENTITY_CUSTOMER,
];
/**
@ -104,6 +113,7 @@ class ImportService
IMPORT_INVOICEPLANE,
IMPORT_NUTCACHE,
IMPORT_RONIN,
IMPORT_STRIPE,
IMPORT_WAVE,
IMPORT_ZOHO,
];
@ -113,6 +123,7 @@ class ImportService
*
* @param Manager $manager
* @param ClientRepository $clientRepo
* @param CustomerRepository $customerRepo
* @param InvoiceRepository $invoiceRepo
* @param PaymentRepository $paymentRepo
* @param ContactRepository $contactRepo
@ -121,6 +132,7 @@ class ImportService
public function __construct(
Manager $manager,
ClientRepository $clientRepo,
CustomerRepository $customerRepo,
InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo,
ContactRepository $contactRepo,
@ -134,6 +146,7 @@ class ImportService
$this->fractal->setSerializer(new ArraySerializer());
$this->clientRepo = $clientRepo;
$this->customerRepo = $customerRepo;
$this->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo;
$this->contactRepo = $contactRepo;
@ -428,8 +441,10 @@ class ImportService
$entity = $this->{"{$entityType}Repo"}->save($data);
// update the entity maps
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
$this->$mapFunction($entity);
if ($entityType != ENTITY_CUSTOMER) {
$mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps';
$this->$mapFunction($entity);
}
// if the invoice is paid we'll also create a payment record
if ($entityType === ENTITY_INVOICE && isset($data['paid']) && $data['paid'] > 0) {
@ -836,6 +851,8 @@ class ImportService
$this->maps = [
'client' => [],
'contact' => [],
'customer' => [],
'invoice' => [],
'invoice_client' => [],
'product' => [],
@ -855,6 +872,16 @@ class ImportService
$this->addClientToMaps($client);
}
$customers = $this->customerRepo->all();
foreach ($customers as $customer) {
$this->addCustomerToMaps($customer);
}
$contacts = $this->contactRepo->all();
foreach ($contacts as $contact) {
$this->addContactToMaps($contact);
}
$invoices = $this->invoiceRepo->all();
foreach ($invoices as $invoice) {
$this->addInvoiceToMaps($invoice);
@ -921,6 +948,25 @@ class ImportService
}
}
/**
* @param Customer $customer
*/
private function addCustomerToMaps(AccountGatewayToken $customer)
{
$this->maps['customer'][$customer->token] = $customer;
$this->maps['customer'][$customer->contact->email] = $customer;
}
/**
* @param Product $product
*/
private function addContactToMaps(Contact $contact)
{
if ($key = strtolower(trim($contact->email))) {
$this->maps['contact'][$key] = $contact;
}
}
/**
* @param Product $product
*/

View File

@ -2426,6 +2426,10 @@ $LANG = array(
'include_errors_help' => 'Include :link from storage/logs/laravel-error.log',
'recent_errors' => 'recent errors',
'add_item' => 'Add Item',
'customer' => 'Customer',
'customers' => 'Customers',
'created_customer' => 'Successfully created customer',
'created_customers' => 'Successfully created :count customers',
);

View File

@ -34,7 +34,8 @@
<br/>
@foreach (\App\Services\ImportService::$entityTypes as $entityType)
{!! Former::file($entityType)
->addGroupClass("import-file {$entityType}-file") !!}
->addGroupClass("import-file {$entityType}-file")
->label(Utils::pluralizeEntityType($entityType)) !!}
@endforeach
<div id="jsonIncludes" style="display:none">