diff --git a/app/Constants.php b/app/Constants.php
index f986319fcdef..1cb133869d66 100644
--- a/app/Constants.php
+++ b/app/Constants.php
@@ -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);
diff --git a/app/Http/Requests/CreateCustomerRequest.php b/app/Http/Requests/CreateCustomerRequest.php
new file mode 100644
index 000000000000..3f490a1a106e
--- /dev/null
+++ b/app/Http/Requests/CreateCustomerRequest.php
@@ -0,0 +1,33 @@
+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;
+ }
+}
diff --git a/app/Http/Requests/CustomerRequest.php b/app/Http/Requests/CustomerRequest.php
new file mode 100644
index 000000000000..d3f7dc69f528
--- /dev/null
+++ b/app/Http/Requests/CustomerRequest.php
@@ -0,0 +1,8 @@
+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
*/
diff --git a/app/Models/Client.php b/app/Models/Client.php
index 4a6d8486e67e..4ff71cee43f0 100644
--- a/app/Models/Client.php
+++ b/app/Models/Client.php
@@ -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',
diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php
index 0f97cf96b64c..76dce0891bf0 100644
--- a/app/Models/PaymentMethod.php
+++ b/app/Models/PaymentMethod.php
@@ -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
*/
diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php
index 850e3d34668c..f1c2d7a70843 100644
--- a/app/Models/PaymentType.php
+++ b/app/Models/PaymentType.php
@@ -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;
+ }
+ }
}
diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php
index 240a36f137a8..63435b45d3bb 100644
--- a/app/Ninja/Import/BaseTransformer.php
+++ b/app/Ninja/Import/BaseTransformer.php
@@ -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
*
diff --git a/app/Ninja/Import/Stripe/CustomerTransformer.php b/app/Ninja/Import/Stripe/CustomerTransformer.php
new file mode 100644
index 000000000000..2fe75bffee51
--- /dev/null
+++ b/app/Ninja/Import/Stripe/CustomerTransformer.php
@@ -0,0 +1,54 @@
+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(),
+ ]
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/PaymentDrivers/BasePaymentDriver.php b/app/Ninja/PaymentDrivers/BasePaymentDriver.php
index b92b30c16be9..d247c2269067 100644
--- a/app/Ninja/PaymentDrivers/BasePaymentDriver.php
+++ b/app/Ninja/PaymentDrivers/BasePaymentDriver.php
@@ -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');
diff --git a/app/Ninja/PaymentDrivers/BraintreePaymentDriver.php b/app/Ninja/PaymentDrivers/BraintreePaymentDriver.php
index 3b3396f3b9ab..ca9486514c62 100644
--- a/app/Ninja/PaymentDrivers/BraintreePaymentDriver.php
+++ b/app/Ninja/PaymentDrivers/BraintreePaymentDriver.php
@@ -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)) {
diff --git a/app/Ninja/PaymentDrivers/StripePaymentDriver.php b/app/Ninja/PaymentDrivers/StripePaymentDriver.php
index fc93b1325069..78fa7cd0be49 100644
--- a/app/Ninja/PaymentDrivers/StripePaymentDriver.php
+++ b/app/Ninja/PaymentDrivers/StripePaymentDriver.php
@@ -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;
diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php
index e7000048c1cd..838b63439877 100644
--- a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php
+++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php
@@ -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;
}
diff --git a/app/Ninja/Repositories/ContactRepository.php b/app/Ninja/Repositories/ContactRepository.php
index 7a72fda3e2d3..992712cfa22b 100644
--- a/app/Ninja/Repositories/ContactRepository.php
+++ b/app/Ninja/Repositories/ContactRepository.php
@@ -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;
diff --git a/app/Ninja/Repositories/CustomerRepository.php b/app/Ninja/Repositories/CustomerRepository.php
new file mode 100644
index 000000000000..d5eb58de4641
--- /dev/null
+++ b/app/Ninja/Repositories/CustomerRepository.php
@@ -0,0 +1,42 @@
+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;
+ }
+}
diff --git a/app/Policies/CustomerPolicy.php b/app/Policies/CustomerPolicy.php
new file mode 100644
index 000000000000..f0ee286cfc69
--- /dev/null
+++ b/app/Policies/CustomerPolicy.php
@@ -0,0 +1,7 @@
+ \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,
];
/**
diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php
index 53433b7ee72c..39229a7a8b90 100644
--- a/app/Services/ImportService.php
+++ b/app/Services/ImportService.php
@@ -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
*/
diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php
index 7e7585fb06d0..49f2012dac28 100644
--- a/resources/lang/en/texts.php
+++ b/resources/lang/en/texts.php
@@ -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',
);
diff --git a/resources/views/accounts/import_export.blade.php b/resources/views/accounts/import_export.blade.php
index dbb640045dc3..1052fcbb3c34 100644
--- a/resources/views/accounts/import_export.blade.php
+++ b/resources/views/accounts/import_export.blade.php
@@ -34,7 +34,8 @@
@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