* Refactor company properties to be presented from settings object instead of company properties

* Working on Email Tests

* Working on emails

* Working on email templats

* Include text version of email

* Refactor Email template builder into trait'

* Fix for custom_value4

* Refactor payment_date -> date && payment_type_id -> type_id

* expose paymentables to API

* expose paymentables to API

* Implement a next_send_date field in invoice/quote tables to allow control over reminder scheduling

* Add custom_values to users,documents and company_gateways tables
This commit is contained in:
David Bomba 2019-12-16 22:34:38 +11:00 committed by GitHub
parent f8551d6119
commit c6e1658ffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 589 additions and 223 deletions

View File

@ -321,11 +321,11 @@ class CreateTestData extends Command
if(rand(0, 1)) { if(rand(0, 1)) {
$payment = PaymentFactory::create($client->company->id, $client->user->id); $payment = PaymentFactory::create($client->company->id, $client->user->id);
$payment->payment_date = now(); $payment->date = now();
$payment->client_id = $client->id; $payment->client_id = $client->id;
$payment->amount = $invoice->balance; $payment->amount = $invoice->balance;
$payment->transaction_reference = rand(0,500); $payment->transaction_reference = rand(0,500);
$payment->payment_type_id = PaymentType::CREDIT_CARD_OTHER; $payment->type_id = PaymentType::CREDIT_CARD_OTHER;
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->save(); $payment->save();

View File

@ -179,6 +179,8 @@ class CompanySettings extends BaseSettings
public $schedule_reminder2 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) public $schedule_reminder2 = ''; // (enum: after_invoice_date, before_due_date, after_due_date)
public $schedule_reminder3 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) public $schedule_reminder3 = ''; // (enum: after_invoice_date, before_due_date, after_due_date)
public $reminder_send_time = 32400; //number of seconds from UTC +0 to send reminders
public $late_fee_amount1 = 0; public $late_fee_amount1 = 0;
public $late_fee_amount2 = 0; public $late_fee_amount2 = 0;
public $late_fee_amount3 = 0; public $late_fee_amount3 = 0;
@ -215,6 +217,7 @@ class CompanySettings extends BaseSettings
public static $casts = [ public static $casts = [
'reminder_send_time' => 'int',
'email_sending_method' => 'string', 'email_sending_method' => 'string',
'gmail_sending_user_id' => 'string', 'gmail_sending_user_id' => 'string',
'currency_id' => 'string', 'currency_id' => 'string',

View File

@ -17,7 +17,7 @@ class EmailTemplateDefaults
{ {
public static function emailInvoiceSubject() public static function emailInvoiceSubject()
{ {
return ctrans('invoice_subject', ['number'=>'$number', 'account'=>'$company']); return ctrans('texts.invoice_subject', ['number'=>'$number', 'account'=>'$company.name']);
//return Parsedown::instance()->line(self::transformText('invoice_subject')); //return Parsedown::instance()->line(self::transformText('invoice_subject'));
} }
@ -28,7 +28,7 @@ class EmailTemplateDefaults
public static function emailQuoteSubject() public static function emailQuoteSubject()
{ {
return ctrans('quote_subject', ['number'=>'$number', 'account'=>'$company']); return ctrans('texts.quote_subject', ['number'=>'$number', 'account'=>'$company.name']);
//return Parsedown::instance()->line(self::transformText('quote_subject')); //return Parsedown::instance()->line(self::transformText('quote_subject'));
} }
@ -51,9 +51,7 @@ class EmailTemplateDefaults
public static function emailReminder1Subject() public static function emailReminder1Subject()
{ {
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']); return ctrans('texts.reminder_subject', ['invoice'=>'$invoice.number', 'account'=>'$company.name']);
// return Parsedown::instance()->line(self::transformText('reminder_subject'));
} }
public static function emailReminder1Template() public static function emailReminder1Template()
@ -63,7 +61,7 @@ class EmailTemplateDefaults
public static function emailReminder2Subject() public static function emailReminder2Subject()
{ {
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']); return ctrans('texts.reminder_subject', ['invoice'=>'$invoice.number', 'account'=>'$company.name']);
// return Parsedown::instance()->line(self::transformText('reminder_subject')); // return Parsedown::instance()->line(self::transformText('reminder_subject'));
} }
@ -74,7 +72,7 @@ class EmailTemplateDefaults
public static function emailReminder3Subject() public static function emailReminder3Subject()
{ {
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']); return ctrans('texts.reminder_subject', ['invoice'=>'$invoice.number', 'account'=>'$company.name']);
// return Parsedown::instance()->line(self::transformText('reminder_subject')); // return Parsedown::instance()->line(self::transformText('reminder_subject'));
} }
@ -85,7 +83,7 @@ class EmailTemplateDefaults
public static function emailReminderEndlessSubject() public static function emailReminderEndlessSubject()
{ {
return ctrans('reminder_subject', ['invoice'=>'$number', 'account'=>'$company']); return ctrans('texts.reminder_subject', ['invoice'=>'$invoice.number', 'account'=>'$company.name']);
// return Parsedown::instance()->line(self::transformText('reminder_subject')); // return Parsedown::instance()->line(self::transformText('reminder_subject'));
} }

View File

@ -20,7 +20,7 @@ class PaymentTransaction
public $account_gateway_id; public $account_gateway_id;
public $payment_type_id; public $type_id;
public $status; // prepayment|payment|response|completed public $status; // prepayment|payment|response|completed

View File

@ -29,10 +29,10 @@ class PaymentFactory
$payment->client_contact_id = null; $payment->client_contact_id = null;
$payment->invitation_id = null; $payment->invitation_id = null;
$payment->company_gateway_id = null; $payment->company_gateway_id = null;
$payment->payment_type_id = null; $payment->type_id = null;
$payment->is_deleted = false; $payment->is_deleted = false;
$payment->amount = 0; $payment->amount = 0;
$payment->payment_date = Carbon::now()->format('Y-m-d'); $payment->date = Carbon::now()->format('Y-m-d');
$payment->transaction_reference = null; $payment->transaction_reference = null;
$payment->payer_id = null; $payment->payer_id = null;
$payment->status_id = Payment::STATUS_PENDING; $payment->status_id = Payment::STATUS_PENDING;

View File

@ -37,7 +37,7 @@ class PaymentFilters extends QueryFilters
return $this->builder->where(function ($query) use ($filter) { return $this->builder->where(function ($query) use ($filter) {
$query->where('payments.amount', 'like', '%'.$filter.'%') $query->where('payments.amount', 'like', '%'.$filter.'%')
->orWhere('payments.payment_date', 'like', '%'.$filter.'%') ->orWhere('payments.date', 'like', '%'.$filter.'%')
->orWhere('payments.custom_value1', 'like', '%'.$filter.'%') ->orWhere('payments.custom_value1', 'like', '%'.$filter.'%')
->orWhere('payments.custom_value2', 'like' , '%'.$filter.'%') ->orWhere('payments.custom_value2', 'like' , '%'.$filter.'%')
->orWhere('payments.custom_value3', 'like' , '%'.$filter.'%') ->orWhere('payments.custom_value3', 'like' , '%'.$filter.'%')

View File

@ -52,20 +52,20 @@ class PaymentController extends Controller
return DataTables::of($payments)->addColumn('action', function ($payment) { return DataTables::of($payments)->addColumn('action', function ($payment) {
return '<a href="/client/payments/'. $payment->hashed_id .'" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i>'.ctrans('texts.view').'</a>'; return '<a href="/client/payments/'. $payment->hashed_id .'" class="btn btn-xs btn-primary"><i class="glyphicon glyphicon-edit"></i>'.ctrans('texts.view').'</a>';
})->editColumn('payment_type_id', function ($payment) { })->editColumn('type_id', function ($payment) {
return $payment->type->name; return $payment->type->name;
}) })
->editColumn('status_id', function ($payment){ ->editColumn('status_id', function ($payment){
return Payment::badgeForStatus($payment->status_id); return Payment::badgeForStatus($payment->status_id);
}) })
->editColumn('payment_date', function ($payment){ ->editColumn('date', function ($payment){
//return $payment->payment_date; //return $payment->date;
return $payment->formatDate($payment->payment_date, $payment->client->date_format()); return $payment->formatDate($payment->date, $payment->client->date_format());
}) })
->editColumn('amount', function ($payment) { ->editColumn('amount', function ($payment) {
return Number::formatMoney($payment->amount, $payment->client); return Number::formatMoney($payment->amount, $payment->client);
}) })
->rawColumns(['action', 'status_id','payment_type_id']) ->rawColumns(['action', 'status_id','type_id'])
->make(true); ->make(true);
} }

View File

@ -37,6 +37,7 @@
* @OA\Property(property="translations", type="object", example="", description="JSON payload of customized translations"), * @OA\Property(property="translations", type="object", example="", description="JSON payload of customized translations"),
* @OA\Property(property="task_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the task number pattern"), * @OA\Property(property="task_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the task number pattern"),
* @OA\Property(property="task_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="task_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="reminder_send_time", type="integer", example="32400", description="Time from UTC +0 when the email will be sent to the client"),
* @OA\Property(property="expense_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the expense number pattern"), * @OA\Property(property="expense_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the expense number pattern"),
* @OA\Property(property="expense_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="expense_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="vendor_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the vendor number pattern"), * @OA\Property(property="vendor_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the vendor number pattern"),

View File

@ -35,6 +35,7 @@
* @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"), * @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"),
* @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"), * @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"),
* @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Invoice Date"), * @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Invoice Date"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"),
* @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"), * @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"),
* @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"), * @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"),
* @OA\Property(property="settings",ref="#/components/schemas/CompanySettings"), * @OA\Property(property="settings",ref="#/components/schemas/CompanySettings"),

View File

@ -4,7 +4,20 @@
* schema="Payment", * schema="Payment",
* type="object", * type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"), * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="client_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="invitation_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="client_contact_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="user_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="type_id", type="string", example="1", description="The Payment Type ID"),
* @OA\Property(property="date", type="string", example="1-1-2014", description="The Payment date"),
* @OA\Property(property="transaction_reference", type="string", example="xcsSxcs124asd", description="The transaction reference as defined by the payment gateway"),
* @OA\Property(property="assigned_user_id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="is_manual", type="boolean", example=true, description="______"), * @OA\Property(property="is_manual", type="boolean", example=true, description="______"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="______"),
* @OA\Property(property="amount", type="number", example=10.00, description="The amount of this payment"),
* @OA\Property(property="refunded", type="number", example=10.00, description="The refunded amount of this payment"), * @OA\Property(property="refunded", type="number", example=10.00, description="The refunded amount of this payment"),
* @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="archived_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="company_gateway_id", type="string", example="3", description="The company gateway id"),
* ) * )
*/ */

View File

@ -5,5 +5,6 @@
* type="object", * type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"), * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"),
* @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"), * @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"),
* ) * )
*/ */

View File

@ -200,7 +200,7 @@ class PaymentController extends BaseController
* format="float", * format="float",
* ), * ),
* @OA\Property( * @OA\Property(
* property="payment_date", * property="date",
* example="2019/12/1", * example="2019/12/1",
* description="The payment date", * description="The payment date",
* type="string", * type="string",

View File

@ -28,7 +28,7 @@ class TemplateController extends BaseController
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
* *
* @OA\Post( * @OA\Post(
* path="/api/v1/templates/{entity}/{entity_id}", * path="/api/v1/templates",
* operationId="getShowTemplate", * operationId="getShowTemplate",
* tags={"templates"}, * tags={"templates"},
* summary="Returns a entity template with the template variables replaced with the Entities", * summary="Returns a entity template with the template variables replaced with the Entities",

View File

@ -75,7 +75,7 @@ class TokenAuth
'errors' => [] 'errors' => []
]; ];
return response()->json(json_encode($error, JinvoicelspSON_PRETTY_PRINT) ,403); return response()->json(json_encode($error, JSON_PRETTY_PRINT) ,403);
} }
return $next($request); return $next($request);

View File

@ -63,7 +63,7 @@ class StorePaymentRequest extends Request
$rules = [ $rules = [
'amount' => 'numeric|required', 'amount' => 'numeric|required',
'payment_date' => 'required', 'date' => 'required',
'client_id' => 'required', 'client_id' => 'required',
'invoices' => new ValidPayableInvoicesRule(), 'invoices' => new ValidPayableInvoicesRule(),
]; ];

View File

@ -35,9 +35,9 @@ class UpdatePaymentRequest extends Request
return [ return [
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
'client_id' => 'integer|nullable', 'client_id' => 'integer|nullable',
'payment_type_id' => 'integer|nullable', 'type_id' => 'integer|nullable',
'amount' => 'numeric', 'amount' => 'numeric',
'payment_date' => 'required', 'date' => 'required',
]; ];
} }

View File

@ -18,11 +18,15 @@ class TemplateEmail extends Mailable
private $user; //the user the email will be sent from private $user; //the user the email will be sent from
public function __construct($message, $template, $user) private $client;
public function __construct($message, $template, $user, $client)
{ {
$this->message = $message; $this->message = $message;
$this->template = $template; $this->template = $template;
$this->user = $user; $this->user = $user; //this is inappropriate here, need to refactor 'user' in this context the 'user' could also be the 'system'
$this->client = $client;
} }
/** /**
@ -37,12 +41,19 @@ class TemplateEmail extends Mailable
//if using a system level template //if using a system level template
$template_name = 'email.template.'.$this->template; $template_name = 'email.template.'.$this->template;
$settings = $this->client->getMergedSettings();
$company = $this->client->company;
return $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version return $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version
->subject($this->message['subject']) ->subject($this->message['subject'])
->text('email.template.plain', ['body' => $this->message['body'], 'footer' => $this->message['footer']])
->view($template_name, [ ->view($template_name, [
'body' => $this->message['body'], 'body' => $this->message['body'],
'footer' => $this->message['footer'], 'footer' => $this->message['footer'],
'title' => $this->message['title'], 'title' => $this->message['title'],
'settings' => $settings,
'company' => $company
]); ]);
} }

View File

@ -75,7 +75,7 @@ class Client extends BaseModel
'custom_value1', 'custom_value1',
'custom_value2', 'custom_value2',
'custom_value3', 'custom_value3',
'custom_value4,', 'custom_value4',
'shipping_address1', 'shipping_address1',
'shipping_address2', 'shipping_address2',
'shipping_city', 'shipping_city',

View File

@ -22,6 +22,7 @@ use App\Models\Currency;
use App\Models\Filterable; use App\Models\Filterable;
use App\Models\PaymentTerm; use App\Models\PaymentTerm;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\InvoiceEmailBuilder;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesInvoiceValues; use App\Utils\Traits\MakesInvoiceValues;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
@ -41,7 +42,8 @@ class Invoice extends BaseModel
use MakesDates; use MakesDates;
use PresentableTrait; use PresentableTrait;
use MakesInvoiceValues; use MakesInvoiceValues;
use InvoiceEmailBuilder;
protected $presenter = 'App\Models\Presenters\InvoicePresenter'; protected $presenter = 'App\Models\Presenters\InvoicePresenter';
protected $hidden = [ protected $hidden = [
@ -461,35 +463,4 @@ class Invoice extends BaseModel
}); });
} }
/**
* @deprecated
*
* we can use the trait -> makeValues()
*
*/
public function getVariables() :array
{
return [
'$number' => $this->number,
'$amount' => $this->amount,
'$date' => $this->date,
'$due_date' => $this->due_date,
'$balance' => $this->balance,
'$status' => $this->textStatus(),
'$invoice.number' => $this->number,
'$invoice.amount' => $this->amount,
'$invoice.date' => $this->date,
'$invoice.due_date' => $this->due_date,
'$invoice.balance' => $this->balance,
'$invoice.status' => $this->textStatus(),
];
}
public function getVariableByKey($key)
{
}
} }

View File

@ -14,6 +14,7 @@ namespace App\Models;
use App\Models\BaseModel; use App\Models\BaseModel;
use App\Models\DateFormat; use App\Models\DateFormat;
use App\Models\Filterable; use App\Models\Filterable;
use App\Models\Paymentable;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -51,9 +52,9 @@ class Payment extends BaseModel
protected $fillable = [ protected $fillable = [
'client_id', 'client_id',
'payment_type_id', 'type_id',
'amount', 'amount',
'payment_date', 'date',
'transaction_reference' 'transaction_reference'
]; ];
@ -102,7 +103,12 @@ class Payment extends BaseModel
public function type() public function type()
{ {
return $this->hasOne(PaymentType::class,'id','payment_type_id'); return $this->hasOne(PaymentType::class,'id','type_id');
}
public function paymentables()
{
return $this->hasMany(Paymentable::class);
} }
public function formattedAmount() public function formattedAmount()
@ -112,12 +118,12 @@ class Payment extends BaseModel
public function clientPaymentDate() public function clientPaymentDate()
{ {
if(!$this->payment_date) if(!$this->date)
return ''; return '';
$date_format = DateFormat::find($this->client->getSetting('date_format_id')); $date_format = DateFormat::find($this->client->getSetting('date_format_id'));
return $this->createClientDate($this->payment_date, $this->client->timezone()->name)->format($date_format->format); return $this->createClientDate($this->date, $this->client->timezone()->name)->format($date_format->format);
} }
public static function badgeForStatus(int $status) public static function badgeForStatus(int $status)

View File

@ -19,5 +19,7 @@ class Paymentable extends Pivot
// public $incrementing = true; // public $incrementing = true;
protected $table = 'paymentables';
} }

View File

@ -11,6 +11,8 @@
namespace App\Models\Presenters; namespace App\Models\Presenters;
use App\Models\Country;
/** /**
* Class CompanyPresenter * Class CompanyPresenter
* @package App\Models\Presenters * @package App\Models\Presenters
@ -26,47 +28,56 @@ class CompanyPresenter extends EntityPresenter
return $this->entity->name ?: ctrans('texts.untitled_account'); return $this->entity->name ?: ctrans('texts.untitled_account');
} }
public function logo() public function logo($settings = null)
{ {
return strlen($this->entity->getLogo() > 0) ? $this->entity->getLogo() : 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png'; if(!$settings)
$settings = $this->entity->settings;
return strlen($settings->company_logo > 0) ? $settings->company_logo : 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png';
} }
public function address() public function address($settings = null)
{ {
$str = ''; $str = '';
$company = $this->entity; $company = $this->entity;
if ($address1 = $company->settings->address1) { if(!$settings)
$settings = $this->entity->settings;
if ($address1 = $settings->address1) {
$str .= e($address1) . '<br/>'; $str .= e($address1) . '<br/>';
} }
if ($address2 = $company->settings->address2) { if ($address2 = $settings->address2) {
$str .= e($address2) . '<br/>'; $str .= e($address2) . '<br/>';
} }
if ($cityState = $this->getCompanyCityState()) { if ($cityState = $this->getCompanyCityState($settings)) {
$str .= e($cityState) . '<br/>'; $str .= e($cityState) . '<br/>';
} }
if ($country = $company->country()) { if ($country = Country::find($settings->country_id)->first()) {
$str .= e($country->name) . '<br/>'; $str .= e($country->name) . '<br/>';
} }
if ($company->settings->phone) { if ($settings->phone) {
$str .= ctrans('texts.work_phone') . ": ". e($company->settings->phone) .'<br/>'; $str .= ctrans('texts.work_phone') . ": ". e($settings->phone) .'<br/>';
} }
if ($company->settings->email) { if ($settings->email) {
$str .= ctrans('texts.work_email') . ": ". e($company->settings->email) .'<br/>'; $str .= ctrans('texts.work_email') . ": ". e($settings->email) .'<br/>';
} }
return $str; return $str;
} }
public function getCompanyCityState() public function getCompanyCityState($settings = null)
{ {
$company = $this->entity; if(!$settings)
$settings = $this->entity->settings;
$swap = $company->country() && $company->country()->swap_postal_code; $country = Country::find($settings->country_id)->first();
$city = e($company->settings->city); $swap = $country && $country->swap_postal_code;
$state = e($company->settings->state);
$postalCode = e($company->settings->postal_code); $city = e($settings->city);
$state = e($settings->state);
$postalCode = e($settings->postal_code);
if ($city || $state || $postalCode) { if ($city || $state || $postalCode) {
return $this->cityStateZip($city, $state, $postalCode, $swap); return $this->cityStateZip($city, $state, $postalCode, $swap);

View File

@ -225,7 +225,7 @@ class BasePaymentDriver
$payment->client_id = $this->client->id; $payment->client_id = $this->client->id;
$payment->company_gateway_id = $this->company_gateway->id; $payment->company_gateway_id = $this->company_gateway->id;
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->payment_date = Carbon::now(); $payment->date = Carbon::now();
return $payment; return $payment;

View File

@ -276,7 +276,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
$client_contact_id = $client_contact ? $client_contact->id : null; $client_contact_id = $client_contact ? $client_contact->id : null;
$payment->amount = $data['PAYMENTINFO_0_AMT']; $payment->amount = $data['PAYMENTINFO_0_AMT'];
$payment->payment_type_id = PaymentType::PAYPAL; $payment->type_id = PaymentType::PAYPAL;
$payment->transaction_reference = $data['PAYMENTINFO_0_TRANSACTIONID']; $payment->transaction_reference = $data['PAYMENTINFO_0_TRANSACTIONID'];
$payment->client_contact_id = $client_contact_id; $payment->client_contact_id = $client_contact_id;
$payment->save(); $payment->save();

View File

@ -408,7 +408,7 @@ class StripePaymentDriver extends BasePaymentDriver
$client_contact_id = $client_contact ? $client_contact->id : null; $client_contact_id = $client_contact ? $client_contact->id : null;
$payment->amount = $this->convertFromStripeAmount($data['amount'], $this->client->currency()->precision); $payment->amount = $this->convertFromStripeAmount($data['amount'], $this->client->currency()->precision);
$payment->payment_type_id = $data['payment_type']; $payment->type_id = $data['payment_type'];
$payment->transaction_reference = $data['payment_method']; $payment->transaction_reference = $data['payment_method'];
$payment->client_contact_id = $client_contact_id; $payment->client_contact_id = $client_contact_id;
$payment->save(); $payment->save();

View File

@ -0,0 +1,56 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Transformers;
use App\Models\Document;
use App\Utils\Traits\MakesHash;
class DocumentTransformer extends EntityTransformer
{
use MakesHash;
protected $serializer;
protected $defaultIncludes = [];
protected $availableIncludes = [];
public function __construct($serializer = null)
{
$this->serializer = $serializer;
}
public function transform(Document $document)
{
return [
'id' => $this->encodePrimaryKey($document->id),
'user_id' => $this->encodePrimaryKey($document->user_id),
'assigned_user_id' => $this->encodePrimaryKey($document->assigned_user_id),
'project_id' => $this->encodePrimaryKey($document->project_id),
'vendor_id' => $this->encodePrimaryKey($document->vendor_id),
'path' => (string) $document->path ?: '',
'preview' => (string) $document->preview ?: '',
'name' => (string) $document->name,
'type' => (string) $document->type,
'disk' => (string) $document->disk,
'hash' => (string) $document->hash,
'size' => (int) $document->size,
'width' => (int) $document->width,
'height' => (int) $document->height,
'is_default' => (bool) $document->is_default,
'updated_at' => (int) $document->updated_at,
'archived_at' => (int) $document->archived_at
];
}
}

View File

@ -96,6 +96,7 @@ class InvoiceTransformer extends EntityTransformer
'discount' => (float) $invoice->discount, 'discount' => (float) $invoice->discount,
'po_number' => $invoice->po_number ?: '', 'po_number' => $invoice->po_number ?: '',
'date' => $invoice->date ?: '', 'date' => $invoice->date ?: '',
'next_send_date' => $invoice->date ?: '',
'due_date' => $invoice->due_date ?: '', 'due_date' => $invoice->due_date ?: '',
'terms' => $invoice->terms ?: '', 'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '', 'public_notes' => $invoice->public_notes ?: '',

View File

@ -15,6 +15,8 @@ use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Paymentable;
use App\Transformers\PaymentableTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
class PaymentTransformer extends EntityTransformer class PaymentTransformer extends EntityTransformer
@ -28,6 +30,7 @@ class PaymentTransformer extends EntityTransformer
protected $availableIncludes = [ protected $availableIncludes = [
'client', 'client',
'invoices', 'invoices',
'paymentables'
]; ];
public function __construct($serializer = null) public function __construct($serializer = null)
@ -50,7 +53,15 @@ class PaymentTransformer extends EntityTransformer
return $this->includeItem($payment->client, $transformer, Client::class); return $this->includeItem($payment->client, $transformer, Client::class);
} }
//todo incomplete
public function includePaymentables(Payment $payment)
{
$transformer = new PaymentableTransformer($this->serializer);
return $this->includeCollection($payment->paymentables, $transformer, Paymentable::class);
}
public function transform(Payment $payment) public function transform(Payment $payment)
{ {
return [ return [
@ -58,14 +69,22 @@ class PaymentTransformer extends EntityTransformer
'user_id' => $this->encodePrimaryKey($payment->user_id), 'user_id' => $this->encodePrimaryKey($payment->user_id),
'assigned_user_id' => $this->encodePrimaryKey($payment->assigned_user_id), 'assigned_user_id' => $this->encodePrimaryKey($payment->assigned_user_id),
'amount' => (float) $payment->amount, 'amount' => (float) $payment->amount,
'refunded' => (float) $payment->refunded,
'transaction_reference' => $payment->transaction_reference ?: '', 'transaction_reference' => $payment->transaction_reference ?: '',
'payment_date' => $payment->payment_date ?: '', 'date' => $payment->date ?: '',
'is_manual' => (bool) $payment->is_manual,
'updated_at' => $payment->updated_at, 'updated_at' => $payment->updated_at,
'archived_at' => $payment->deleted_at, 'archived_at' => $payment->deleted_at,
'is_deleted' => (bool) $payment->is_deleted, 'is_deleted' => (bool) $payment->is_deleted,
'payment_type_id' => (string) $payment->payment_type_id ?: '', 'type_id' => (string) $payment->payment_type_id ?: '',
'invitation_id' => (string) $payment->invitation_id ?: '', 'invitation_id' => (string) $payment->invitation_id ?: '',
'client_id' => (string) $this->encodePrimaryKey($payment->client_id), 'client_id' => (string) $this->encodePrimaryKey($payment->client_id),
'client_contact_id' => (string) $this->encodePrimaryKey($payment->client_contact_id),
'company_gateway_id' => (string) $this->encodePrimaryKey($payment->company_gateway_id),
'status_id'=> (string) $payment->status_id,
'type_id'=> (string) $payment->type_id,
'project_id' => (string) $this->encodePrimaryKey($payment->project_id),
'vendor_id' => (string) $this->encodePrimaryKey($payment->vendor_id),
/* /*
'private_notes' => $payment->private_notes ?: '', 'private_notes' => $payment->private_notes ?: '',
'exchange_rate' => (float) $payment->exchange_rate, 'exchange_rate' => (float) $payment->exchange_rate,

View File

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Transformers;
use App\Models\Payment;
use App\Models\Paymentable;
use App\Utils\Traits\MakesHash;
class PaymentableTransformer extends EntityTransformer
{
use MakesHash;
protected $serializer;
protected $defaultIncludes = [];
protected $availableIncludes = [];
public function __construct($serializer = null)
{
$this->serializer = $serializer;
}
public function transform(Paymentable $paymentable)
{
return [
'id' => $this->encodePrimaryKey($paymentable->id),
'invoice_id' => $this->encodePrimaryKey($paymentable->paymentable_id),
'amount' => $paymentable->amount,
];
}
}

View File

@ -93,6 +93,7 @@ class QuoteTransformer extends EntityTransformer
'discount' => (float) $quote->discount ?: '', 'discount' => (float) $quote->discount ?: '',
'po_number' => $quote->po_number ?: '', 'po_number' => $quote->po_number ?: '',
'quote_date' => $quote->quote_date ?: '', 'quote_date' => $quote->quote_date ?: '',
'next_send_date' => $quote->date ?: '',
'valid_until' => $quote->valid_until ?: '', 'valid_until' => $quote->valid_until ?: '',
'terms' => $quote->terms ?: '', 'terms' => $quote->terms ?: '',
'public_notes' => $quote->public_notes ?: '', 'public_notes' => $quote->public_notes ?: '',

View File

@ -0,0 +1,120 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils\Traits;
use App\Models\Invoice;
use Illuminate\Support\Carbon;
use Parsedown;
/**
* Class InvoiceEmailBuilder
* @package App\Utils\Traits
*/
trait InvoiceEmailBuilder
{
/**
* Builds the correct template to send
* @param string $reminder_template The template name ie reminder1
* @return array
*/
public function getEmailData($reminder_template = null) :array
{
//client
$client = $this->client;
if(!$reminder_template)
$reminder_template = $this->calculateTemplate();
//Need to determine which email template we are producing
$email_data = $this->generateTemplateData($reminder_template);
return $email_data;
}
private function generateTemplateData(string $reminder_template) :array
{
$data = [];
$client = $this->client;
$body_template = $client->getSetting('email_template_'.$reminder_template);
$subject_template = $client->getSetting('email_subject_'.$reminder_template);
$data['body'] = $this->parseTemplate($body_template, false);
$data['subject'] = $this->parseTemplate($subject_template, true);
return $data;
}
private function parseTemplate(string $template_data, bool $is_markdown = true) :string
{
$invoice_variables = $this->makeValues();
//process variables
$data = str_replace(array_keys($invoice_variables), array_values($invoice_variables), $template_data);
//process markdown
if($is_markdown)
$data = Parsedown::instance()->line($data);
return $data;
}
private function calculateTemplate() :string
{
//if invoice is currently a draft, or being marked as sent, this will be the initial email
$client = $this->client;
//if the invoice
if($this->status_id == Invoice::STATUS_DRAFT || Carbon::parse($this->due_date) > now())
{
return 'invoice';
}
else if($client->getSetting('enable_reminder1') !== false && $this->inReminderWindow($client->getSetting('schedule_reminder1'), $client->getSetting('num_days_reminder1')))
{
return 'template1';
}
else if($client->getSetting('enable_reminder2') !== false && $this->inReminderWindow($client->getSetting('schedule_reminder2'), $client->getSetting('num_days_reminder2')))
{
return 'template2';
}
else if($client->getSetting('enable_reminder3') !== false && $this->inReminderWindow($client->getSetting('schedule_reminder3'), $client->getSetting('num_days_reminder3')))
{
return 'template3';
}
//also implement endless reminders here
//
}
private function inReminderWindow($schedule_reminder, $num_days_reminder)
{
switch ($schedule_reminder) {
case 'after_invoice_date':
return Carbon::parse($this->date)->addDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
case 'before_due_date':
return Carbon::parse($this->due_date)->subDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
case 'after_due_date':
return Carbon::parse($this->due_date)->addDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
default:
# code...
break;
}
}
}

View File

@ -11,6 +11,7 @@
namespace App\Utils\Traits; namespace App\Utils\Traits;
use App\Models\Country;
use App\Utils\Number; use App\Utils\Number;
/** /**
@ -158,7 +159,9 @@ trait MakesInvoiceValues
throw new Exception(debug_backtrace()[1]['function'], 1); throw new Exception(debug_backtrace()[1]['function'], 1);
exit; exit;
} }
$settings = $this->client->getMergedSettings();
$data = []; $data = [];
$data['$date'] = $this->date; $data['$date'] = $this->date;
@ -166,9 +169,13 @@ trait MakesInvoiceValues
$data['$due_date'] = $this->due_date; $data['$due_date'] = $this->due_date;
$data['$invoice.due_date'] = &$data['$due_date']; $data['$invoice.due_date'] = &$data['$due_date'];
$data['$number'] = $this->number; $data['$number'] = $this->number;
$data['$invoice.number'] = &$data['$number'];
$data['$po_number'] = $this->po_number; $data['$po_number'] = $this->po_number;
$data['$invoice.po_number'] = &$data['$po_number'];
$data['$line_taxes'] = $this->makeLineTaxes(); $data['$line_taxes'] = $this->makeLineTaxes();
$data['$invoice.line_taxes'] = &$data['$line_taxes'];
$data['$total_taxes'] = $this->makeTotalTaxes(); $data['$total_taxes'] = $this->makeTotalTaxes();
$data['$invoice.total_taxes'] = &$data['$total_taxes'];
// $data['$tax'] = ; // $data['$tax'] = ;
// $data['$item'] = ; // $data['$item'] = ;
// $data['$description'] = ; // $data['$description'] = ;
@ -179,12 +186,22 @@ trait MakesInvoiceValues
$data['$discount'] = Number::formatMoney($this->calc()->getTotalDiscount(), $this->client); $data['$discount'] = Number::formatMoney($this->calc()->getTotalDiscount(), $this->client);
$data['$invoice.discount'] = &$data['$discount']; $data['$invoice.discount'] = &$data['$discount'];
$data['$subtotal'] = Number::formatMoney($this->calc()->getSubTotal(), $this->client); $data['$subtotal'] = Number::formatMoney($this->calc()->getSubTotal(), $this->client);
$data['$invoice.subtotal'] = &$data['$subtotal'];
$data['$balance_due'] = Number::formatMoney($this->balance, $this->client); $data['$balance_due'] = Number::formatMoney($this->balance, $this->client);
$data['$invoice.balance_due'] = &$data['$balance_due'];
$data['$partial_due'] = Number::formatMoney($this->partial, $this->client); $data['$partial_due'] = Number::formatMoney($this->partial, $this->client);
$data['$invoice.partial_due'] = &$data['$partial_due'];
$data['$total'] = Number::formatMoney($this->calc()->getTotal(), $this->client); $data['$total'] = Number::formatMoney($this->calc()->getTotal(), $this->client);
$data['$invoice.total'] = &$data['$total'];
$data['$amount'] = &$data['$total'];
$data['$invoice.amount'] = &$data['$total'];
$data['$balance'] = Number::formatMoney($this->calc()->getBalance(), $this->client); $data['$balance'] = Number::formatMoney($this->calc()->getBalance(), $this->client);
$data['$invoice.balance'] = &$data['$balance'];
$data['$taxes'] = Number::formatMoney($this->calc()->getItemTotalTaxes(), $this->client); $data['$taxes'] = Number::formatMoney($this->calc()->getItemTotalTaxes(), $this->client);
$data['$invoice.taxes'] = &$data['$taxes'];
$data['$terms'] = $this->terms; $data['$terms'] = $this->terms;
$data['$invoice.terms'] = &$data['$terms'];
// $data['$your_invoice'] = ; // $data['$your_invoice'] = ;
// $data['$quote'] = ; // $data['$quote'] = ;
// $data['$your_quote'] = ; // $data['$your_quote'] = ;
@ -200,34 +217,48 @@ trait MakesInvoiceValues
// $data['$quote_to'] = ; // $data['$quote_to'] = ;
// $data['$details'] = ; // $data['$details'] = ;
$data['$invoice_no'] = $this->number; $data['$invoice_no'] = $this->number;
$data['$invoice.invoice_no'] = &$data['$invoice_no'];
// $data['$quote_no'] = ; // $data['$quote_no'] = ;
// $data['$valid_until'] = ; // $data['$valid_until'] = ;
$data['$client_name'] = $this->present()->clientName(); $data['$client_name'] = $this->present()->clientName();
$data['$client.name'] = &$data['$client_name'];
$data['$client_address'] = $this->present()->address(); $data['$client_address'] = $this->present()->address();
$data['$client.address'] = &$data['$client_address'];
$data['$address1'] = $this->client->address1; $data['$address1'] = $this->client->address1;
$data['$client.address1'] = &$data['$address1'];
$data['$address2'] = $this->client->address2; $data['$address2'] = $this->client->address2;
$data['$client.address2'] = &$data['$address2'];
$data['$id_number'] = $this->client->id_number; $data['$id_number'] = $this->client->id_number;
$data['$client.id_number'] = &$data['$id_number'];
$data['$vat_number'] = $this->client->vat_number; $data['$vat_number'] = $this->client->vat_number;
$data['$client.vat_number'] = &$data['$vat_number'];
$data['$website'] = $this->client->present()->website(); $data['$website'] = $this->client->present()->website();
$data['$client.website'] = &$data['$website'];
$data['$phone'] = $this->client->present()->phone(); $data['$phone'] = $this->client->present()->phone();
$data['$client.phone'] = &$data['$phone'];
$data['$city_state_postal'] = $this->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, FALSE); $data['$city_state_postal'] = $this->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, FALSE);
$data['$client.city_state_postal'] = &$data['$city_state_postal'];
$data['$postal_city_state'] = $this->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, TRUE); $data['$postal_city_state'] = $this->present()->cityStateZip($this->client->city, $this->client->state, $this->client->postal_code, TRUE);
$data['$client.postal_city_state'] = &$data['$postal_city_state'];
$data['$country'] = isset($this->client->country->name) ?: 'No Country Set'; $data['$country'] = isset($this->client->country->name) ?: 'No Country Set';
$data['$client.country'] = &$data['$country'];
$data['$email'] = isset($this->client->primary_contact()->first()->email) ?: 'no contact email on record'; $data['$email'] = isset($this->client->primary_contact()->first()->email) ?: 'no contact email on record';
$data['$client.email'] = &$data['$email'];
$data['$contact_name'] = $this->client->present()->primary_contact_name(); $data['$contact_name'] = $this->client->present()->primary_contact_name();
$data['$company_name'] = $this->company->present()->name(); $data['$contact.name'] = &$data['$contact_name'];
$data['$company_address1'] = $this->company->address1; $data['$company.name'] = $this->company->present()->name();
$data['$company_address2'] = $this->company->address2; $data['$company.address1'] = $settings->address1;
$data['$company_city'] = $this->company->city; $data['$company.address2'] = $settings->address2;
$data['$company_state'] = $this->company->state; $data['$company.city'] = $settings->city;
$data['$company_postal_code'] = $this->company->postal_code; $data['$company.state'] = $settings->state;
$data['$company_country'] = $this->company->country() ? $this->company->country()->name : ''; $data['$company.postal_code'] = $settings->postal_code;
$data['$company_phone'] = $this->company->phone; $data['$company.country'] = Country::find($settings->country_id)->first()->name;
$data['$company_email'] = $this->company->email; $data['$company.phone'] = $settings->phone;
$data['$company_vat_number'] = $this->company->vat_number; $data['$company.email'] = $settings->email;
$data['$company_id_number'] = $this->company->id_number; $data['$company.vat_number'] = $settings->vat_number;
$data['$company_address'] = $this->company->present()->address(); $data['$company.id_number'] = $settings->id_number;
$data['$company_logo'] = $this->company->present()->logo(); $data['$company.address'] = $this->company->present()->address($settings);
$data['$company.logo'] = $this->company->present()->logo($settings);
//$data['$blank'] = ; //$data['$blank'] = ;
//$data['$surcharge'] = ; //$data['$surcharge'] = ;
/* /*

View File

@ -10,9 +10,9 @@ $factory->define(App\Models\Payment::class, function (Faker $faker) {
return [ return [
'is_deleted' => false, 'is_deleted' => false,
'amount' => $faker->numberBetween(1,10), 'amount' => $faker->numberBetween(1,10),
'payment_date' => $faker->date(), 'date' => $faker->date(),
'transaction_reference' => $faker->text(10), 'transaction_reference' => $faker->text(10),
'payment_type_id' => Payment::TYPE_CREDIT_CARD, 'type_id' => Payment::TYPE_CREDIT_CARD,
'status_id' => Payment::STATUS_COMPLETED 'status_id' => Payment::STATUS_COMPLETED
]; ];

View File

@ -211,7 +211,10 @@ class CreateUsersTable extends Migration
Schema::create('documents', function (Blueprint $table){ Schema::create('documents', function (Blueprint $table){
$table->increments('id'); $table->increments('id');
$table->unsignedInteger('user_id'); $table->unsignedInteger('user_id');
$table->unsignedInteger('assigned_user_id');
$table->unsignedInteger('company_id')->index(); $table->unsignedInteger('company_id')->index();
$table->unsignedInteger('project_id')->nullable();
$table->unsignedInteger('vendor_id')->nullable();
$table->string('path')->nullable(); $table->string('path')->nullable();
$table->string('preview')->nullable(); $table->string('preview')->nullable();
$table->string('name')->nullable(); $table->string('name')->nullable();
@ -222,6 +225,10 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('width')->nullable(); $table->unsignedInteger('width')->nullable();
$table->unsignedInteger('height')->nullable(); $table->unsignedInteger('height')->nullable();
$table->boolean('is_default')->default(0); $table->boolean('is_default')->default(0);
$table->string('custom_value1')->nullable();
$table->string('custom_value2')->nullable();
$table->string('custom_value3')->nullable();
$table->string('custom_value4')->nullable();
$table->unsignedInteger('documentable_id'); $table->unsignedInteger('documentable_id');
$table->string('documentable_type'); $table->string('documentable_type');
@ -258,7 +265,11 @@ class CreateUsersTable extends Migration
$table->mediumText('signature')->nullable(); $table->mediumText('signature')->nullable();
$table->string('password'); $table->string('password');
$table->rememberToken(); $table->rememberToken();
$table->string('custom_value1')->nullable();
$table->string('custom_value2')->nullable();
$table->string('custom_value3')->nullable();
$table->string('custom_value4')->nullable();
$table->timestamps(6); $table->timestamps(6);
$table->softDeletes('deleted_at', 6); $table->softDeletes('deleted_at', 6);
@ -378,6 +389,22 @@ class CreateUsersTable extends Migration
}); });
Schema::create('projects', function ($t) {
$t->increments('id');
$t->unsignedInteger('user_id');
$t->unsignedInteger('assigned_user_id');
$t->unsignedInteger('company_id')->index();
$t->unsignedInteger('client_id')->nullable();
$t->string('name');
$t->string('description');
$t->timestamps();
$t->softDeletes();
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$t->foreign('company_id')->references('id')->on('companies');
});
Schema::create('company_gateways', function($table) Schema::create('company_gateways', function($table)
{ {
$table->increments('id'); $table->increments('id');
@ -391,7 +418,11 @@ class CreateUsersTable extends Migration
$table->boolean('update_details')->default(false)->nullable(); $table->boolean('update_details')->default(false)->nullable();
$table->mediumText('config'); $table->mediumText('config');
$table->text('fees_and_limits'); $table->text('fees_and_limits');
$table->string('custom_value1')->nullable();
$table->string('custom_value2')->nullable();
$table->string('custom_value3')->nullable();
$table->string('custom_value4')->nullable();
$table->timestamps(6); $table->timestamps(6);
$table->softDeletes('deleted_at', 6); $table->softDeletes('deleted_at', 6);
@ -410,7 +441,8 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('status_id'); $t->unsignedInteger('status_id');
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('recurring_id')->nullable(); $t->unsignedInteger('recurring_id')->nullable();
$t->unsignedInteger('design_id')->nullable(); $t->unsignedInteger('design_id')->nullable();
@ -449,6 +481,7 @@ class CreateUsersTable extends Migration
$t->string('custom_value2')->nullable(); $t->string('custom_value2')->nullable();
$t->string('custom_value3')->nullable(); $t->string('custom_value3')->nullable();
$t->string('custom_value4')->nullable(); $t->string('custom_value4')->nullable();
$t->datetime('next_send_date')->nullable();
$t->string('custom_surcharge1')->nullable(); $t->string('custom_surcharge1')->nullable();
$t->string('custom_surcharge2')->nullable(); $t->string('custom_surcharge2')->nullable();
@ -482,6 +515,8 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('user_id'); $t->unsignedInteger('user_id');
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('status_id')->index(); $t->unsignedInteger('status_id')->index();
$t->text('number')->nullable(); $t->text('number')->nullable();
@ -548,7 +583,8 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('user_id'); $t->unsignedInteger('user_id');
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('status_id')->index(); $t->unsignedInteger('status_id')->index();
$t->float('discount')->default(0); $t->float('discount')->default(0);
@ -613,7 +649,8 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('status_id'); $t->unsignedInteger('status_id');
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('recurring_id')->nullable(); $t->unsignedInteger('recurring_id')->nullable();
$t->unsignedInteger('design_id')->nullable(); $t->unsignedInteger('design_id')->nullable();
@ -624,6 +661,7 @@ class CreateUsersTable extends Migration
$t->string('po_number')->nullable(); $t->string('po_number')->nullable();
$t->date('date')->nullable(); $t->date('date')->nullable();
$t->datetime('due_date')->nullable(); $t->datetime('due_date')->nullable();
$t->datetime('next_send_date')->nullable();
$t->boolean('is_deleted')->default(false); $t->boolean('is_deleted')->default(false);
@ -731,7 +769,8 @@ class CreateUsersTable extends Migration
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('user_id'); $t->unsignedInteger('user_id');
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->string('custom_value1')->nullable(); $t->string('custom_value1')->nullable();
$t->string('custom_value2')->nullable(); $t->string('custom_value2')->nullable();
$t->string('custom_value3')->nullable(); $t->string('custom_value3')->nullable();
@ -765,16 +804,18 @@ class CreateUsersTable extends Migration
$t->increments('id'); $t->increments('id');
$t->unsignedInteger('company_id')->index(); $t->unsignedInteger('company_id')->index();
$t->unsignedInteger('client_id')->index(); $t->unsignedInteger('client_id')->index();
$t->unsignedInteger('project_id')->nullable();
$t->unsignedInteger('vendor_id')->nullable();
$t->unsignedInteger('user_id')->nullable(); $t->unsignedInteger('user_id')->nullable();
$t->unsignedInteger('assigned_user_id')->nullable(); $t->unsignedInteger('assigned_user_id')->nullable();
$t->unsignedInteger('client_contact_id')->nullable(); $t->unsignedInteger('client_contact_id')->nullable();
$t->unsignedInteger('invitation_id')->nullable(); $t->unsignedInteger('invitation_id')->nullable();
$t->unsignedInteger('company_gateway_id')->nullable(); $t->unsignedInteger('company_gateway_id')->nullable();
$t->unsignedInteger('payment_type_id')->nullable(); $t->unsignedInteger('type_id')->nullable();
$t->unsignedInteger('status_id')->index(); $t->unsignedInteger('status_id')->index();
$t->decimal('amount', 16, 4)->default(0); $t->decimal('amount', 16, 4)->default(0);
$t->decimal('refunded', 16, 4)->default(0); $t->decimal('refunded', 16, 4)->default(0);
$t->datetime('payment_date')->nullable(); $t->date('date')->nullable();
$t->string('transaction_reference')->nullable(); $t->string('transaction_reference')->nullable();
$t->string('payer_id')->nullable(); $t->string('payer_id')->nullable();
$t->timestamps(6); $t->timestamps(6);
@ -788,7 +829,7 @@ class CreateUsersTable extends Migration
$t->foreign('company_gateway_id')->references('id')->on('company_gateways')->onDelete('cascade'); $t->foreign('company_gateway_id')->references('id')->on('company_gateways')->onDelete('cascade');
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$t->foreign('payment_type_id')->references('id')->on('payment_types'); $t->foreign('type_id')->references('id')->on('payment_types');
}); });
@ -819,6 +860,9 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('company_id')->index(); $table->unsignedInteger('company_id')->index();
$table->unsignedInteger('client_id')->nullable(); $table->unsignedInteger('client_id')->nullable();
$table->unsignedInteger('invoice_id')->nullable(); $table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('project_id')->nullable();
$table->unsignedInteger('vendor_id')->nullable();
$table->timestamps(6); $table->timestamps(6);
$table->softDeletes('deleted_at', 6); $table->softDeletes('deleted_at', 6);
@ -902,6 +946,8 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('client_id')->nullable(); $table->unsignedInteger('client_id')->nullable();
$table->unsignedInteger('client_contact_id')->nullable(); $table->unsignedInteger('client_contact_id')->nullable();
$table->unsignedInteger('account_id')->nullable(); $table->unsignedInteger('account_id')->nullable();
$table->unsignedInteger('project_id')->nullable();
$table->unsignedInteger('vendor_id')->nullable();
$table->unsignedInteger('payment_id')->nullable(); $table->unsignedInteger('payment_id')->nullable();
$table->unsignedInteger('invoice_id')->nullable(); $table->unsignedInteger('invoice_id')->nullable();
$table->unsignedInteger('invitation_id')->nullable(); $table->unsignedInteger('invitation_id')->nullable();
@ -914,6 +960,8 @@ class CreateUsersTable extends Migration
$table->text('notes'); $table->text('notes');
$table->timestamps(6); $table->timestamps(6);
$table->index(['vendor_id', 'company_id']);
$table->index(['project_id', 'company_id']);
$table->index(['user_id', 'company_id']); $table->index(['user_id', 'company_id']);
$table->index(['client_id', 'company_id']); $table->index(['client_id', 'company_id']);
$table->index(['payment_id', 'company_id']); $table->index(['payment_id', 'company_id']);

View File

@ -168,13 +168,13 @@ class RandomDataSeeder extends Seeder
if(rand(0, 1)) { if(rand(0, 1)) {
$payment = App\Models\Payment::create([ $payment = App\Models\Payment::create([
'payment_date' => now(), 'date' => now(),
'user_id' => $user->id, 'user_id' => $user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'client_id' => $client->id, 'client_id' => $client->id,
'amount' => $invoice->balance, 'amount' => $invoice->balance,
'transaction_reference' => rand(0,500), 'transaction_reference' => rand(0,500),
'payment_type_id' => PaymentType::CREDIT_CARD_OTHER, 'type_id' => PaymentType::CREDIT_CARD_OTHER,
'status_id' => Payment::STATUS_COMPLETED, 'status_id' => Payment::STATUS_COMPLETED,
]); ]);

View File

@ -0,0 +1,11 @@
@if ($company->present()->logo($settings))
@if ($settings->website)
<a href="{{ $settings->website }}" style="color: #19BB40; text-decoration: underline;">
@endif
<img src="{{ $company->present()->logo() }}" height="50" style="height:50px; max-width:140px; margin-left: 33px; padding-top: 2px" alt=""/>
@if ($settings->website)
</a>
@endif
@endif

View File

@ -304,6 +304,9 @@
<body class=""> <body class="">
<span class="preheader">This is preheader text. Some clients will show this text as a preview.</span> <span class="preheader">This is preheader text. Some clients will show this text as a preview.</span>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>@include('email.partials.company_logo')</td>
</tr>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td class="container"> <td class="container">

View File

@ -1,4 +1,4 @@
{{ $body }} {{ $body }}
<br>
<br>
{{ $footer }} {{ $footer }}

View File

@ -2,10 +2,19 @@
namespace Feature; namespace Feature;
use App\Mail\TemplateEmail;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\InvoiceEmailBuilder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase; use Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Parsedown;
use Tests\MockAccountData; use Tests\MockAccountData;
use Tests\TestCase; use Tests\TestCase;
@ -16,6 +25,7 @@ class InvoiceEmailTest extends TestCase
{ {
use MockAccountData; use MockAccountData;
use DatabaseTransactions; use DatabaseTransactions;
use GeneratesCounter;
public function setUp() :void public function setUp() :void
{ {
@ -33,119 +43,62 @@ class InvoiceEmailTest extends TestCase
public function test_initial_email_sends() public function test_initial_email_sends()
{ {
\Log::error($this->invoice->makeValues()); // \Log::error($this->invoice->makeValues());
}
$this->invoice->date = now();
$this->invoice->due_date = now()->addDays(7);
$this->invoice->number = $this->getNextInvoiceNumber($this->client);
$this->invoice->client = $this->client;
$message_array = $this->invoice->getEmailData();
$message_array['title'] = &$message_array['subject'];
$message_array['footer'] = 'The Footer';
$template_style = $this->client->getSetting('email_style');
$template_style = 'light';
//iterate through the senders list and send from here
$invitations = InvoiceInvitation::whereInvoiceId($this->invoice->id)->get();
$invitations->each(function($invitation) use($message_array, $template_style) {
$contact = ClientContact::find($invitation->client_contact_id)->first();
if($contact->send_invoice && $contact->email)
{
//there may be template variables left over for the specific contact? need to reparse here
//change the runtime config of the mail provider here:
//send message
Mail::to($contact->email)
->send(new TemplateEmail($message_array, $template_style, $this->user, $this->client));
//fire any events
sleep(5);
}
});
//TDD
/**
* Builds the correct template to send
* @param App\Models\Invoice $invoice The Invoice Model
* @param string $reminder_template The template name ie reminder1
* @return void
*/
private function invoiceEmailWorkFlow($invoice, $reminder_template = null)
{
//client
$client = $invoice->client;
$template_style = $client->getSetting('email_style');
if(!$reminder_template)
$reminder_template = $this->calculateTemplate($invoice);
//Need to determine which email template we are producing
$email_data = $this->generateTemplateData($invoice, $reminder_template);
}
private function generateTemplateData(Invoice $invoice, string $reminder_template) :array
{
$data = [];
$client = $invoice->client;
$body_template = $client->getSetting('email_template_'.$reminder_template);
$subject_template = $client->getSetting('email_subject_'.$reminder_template);
$data['message'] = $this->parseTemplate($invoice, $body_template);
$data['subject'] = $this->parseTemplate($invoice, $subject_template);
return $data;
}
private function parseTemplate($invoice, $template_data) :string
{
//process variables
//process markdown
} }
private function calculateTemplate(Invoice $invoice) :string
{
//if invoice is currently a draft, or being marked as sent, this will be the initial email
$client = $invoice->client;
//if the invoice
if($invoice->status_id == Invoice::STATUS_DRAFT || Carbon::parse($invoice->due_date) > now())
{
return 'invoice';
}
else if($client->getSetting('enable_reminder1') !== false && $this->inReminderWindow($invoice, $client->getSetting('schedule_reminder1'), $client->getSetting('num_days_reminder1')))
{
return 'template1';
}
else if($client->getSetting('enable_reminder2') !== false && $this->inReminderWindow($invoice, $client->getSetting('schedule_reminder2'), $client->getSetting('num_days_reminder2')))
{
return 'template2';
}
else if($client->getSetting('enable_reminder3') !== false && $this->inReminderWindow($invoice, $client->getSetting('schedule_reminder3'), $client->getSetting('num_days_reminder3')))
{
return 'template3';
}
//also implement endless reminders here
//
}
private function inReminderWindow($invoice, $schedule_reminder, $num_days_reminder)
{
switch ($schedule_reminder) {
case 'after_invoice_date':
return Carbon::parse($invoice->date)->addDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
case 'before_due_date':
return Carbon::parse($invoice->due_date)->subDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
case 'after_due_date':
return Carbon::parse($invoice->due_date)->addDays($num_days_reminder)->startOfDay()->eq(Carbon::now()->startOfDay());
break;
default:
# code...
break;
}
}
} }

View File

@ -135,7 +135,7 @@ class PaymentTest extends TestCase
'amount' => $this->invoice->amount 'amount' => $this->invoice->amount
], ],
], ],
'payment_date' => '2020/12/11', 'date' => '2020/12/11',
]; ];
@ -185,7 +185,7 @@ class PaymentTest extends TestCase
'amount' => $this->invoice->amount 'amount' => $this->invoice->amount
], ],
], ],
'payment_date' => '2020/12/12', 'date' => '2020/12/12',
]; ];
@ -245,11 +245,13 @@ class PaymentTest extends TestCase
'amount' => $this->invoice->amount, 'amount' => $this->invoice->amount,
'client_id' => $client->hashed_id, 'client_id' => $client->hashed_id,
'invoices' => '', 'invoices' => '',
'payment_date' => '2020/12/12', 'date' => '2020/12/12',
]; ];
$response = false;
try { try {
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
@ -300,18 +302,31 @@ class PaymentTest extends TestCase
'amount' => 2.0 'amount' => 2.0
], ],
], ],
'payment_date' => '2019/12/12', 'date' => '2019/12/12',
]; ];
$response = false;
try {
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token, 'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments?include=invoices', $data); ])->post('/api/v1/payments?include=invoices', $data);
$arr = $response->json();
}
catch(ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(),1);
$this->assertNotNull($message);
\Log::error($message);
}
if($response) {
$response->assertStatus(200); $response->assertStatus(200);
$arr = $response->json();
$payment_id = $arr['data']['id']; $payment_id = $arr['data']['id'];
$payment = Payment::find($this->decodePrimaryKey($payment_id))->first(); $payment = Payment::find($this->decodePrimaryKey($payment_id))->first();
@ -325,6 +340,7 @@ class PaymentTest extends TestCase
$this->assertEquals($pivot_invoice->partial, 0); $this->assertEquals($pivot_invoice->partial, 0);
$this->assertEquals($pivot_invoice->amount, 10.0000); $this->assertEquals($pivot_invoice->amount, 10.0000);
$this->assertEquals($pivot_invoice->balance, 8.0000); $this->assertEquals($pivot_invoice->balance, 8.0000);
}
} }
@ -364,7 +380,7 @@ class PaymentTest extends TestCase
'amount' => 6.0 'amount' => 6.0
], ],
], ],
'payment_date' => '2019/12/12', 'date' => '2019/12/12',
]; ];
$response = $this->withHeaders([ $response = $this->withHeaders([
@ -425,7 +441,7 @@ class PaymentTest extends TestCase
'amount' => 2.0 'amount' => 2.0
], ],
], ],
'payment_date' => '2019/12/12', 'date' => '2019/12/12',
]; ];
$response = $this->withHeaders([ $response = $this->withHeaders([

View File

@ -17,16 +17,19 @@ use App\DataMapper\DefaultSettings;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Factory\CompanyUserFactory; use App\Factory\CompanyUserFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\InvoiceInvitationFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Factory\InvoiceToRecurringInvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice; use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Jobs\Invoice\CreateInvoiceInvitations;
use App\Models\Client; use App\Models\Client;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\CompanyToken; use App\Models\CompanyToken;
use App\Models\Credit; use App\Models\Credit;
use App\Models\GroupSetting; use App\Models\GroupSetting;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\User; use App\Models\User;
@ -127,13 +130,33 @@ trait MockAccountData
// 'settings' => json_encode(DefaultSettings::userSettings()), // 'settings' => json_encode(DefaultSettings::userSettings()),
// ]); // ]);
$this->client = ClientFactory::create($this->company->id, $this->user->id); $this->client = ClientFactory::create($this->company->id, $this->user->id);
$this->client->save(); $this->client->save();
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'is_primary' => 1,
'send_invoice' => true,
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'company_id' => $this->company->id,
'send_invoice' => true
]);
$gs = new GroupSetting; $gs = new GroupSetting;
$gs->name = 'Test'; $gs->name = 'Test';
$gs->company_id = $this->client->company_id; $gs->company_id = $this->client->company_id;
$gs->settings = ClientSettings::buildClientSettings($this->company->settings, $this->client->settings); $gs->settings = ClientSettings::buildClientSettings($this->company->settings, $this->client->settings);
$gs_settings = $gs->settings;
$gs_settings->website = 'http://staging.invoicing.co';
$gs->settings = $gs_settings;
$gs->save(); $gs->save();
$this->client->group_settings_id = $gs->id; $this->client->group_settings_id = $gs->id;
@ -154,6 +177,29 @@ trait MockAccountData
$this->invoice->save(); $this->invoice->save();
$this->invoice->markSent();
$contacts = $this->invoice->client->contacts;
$contacts->each(function ($contact) {
$invitation = InvoiceInvitation::whereCompanyId($this->invoice->company_id)
->whereClientContactId($contact->id)
->whereInvoiceId($this->invoice->id)
->first();
if(!$invitation && $contact->send_invoice) {
$ii = InvoiceInvitationFactory::create($this->invoice->company_id, $this->invoice->user_id);
$ii->invoice_id = $this->invoice->id;
$ii->client_contact_id = $contact->id;
$ii->save();
}
else if($invitation && !$contact->send_invoice) {
$invitation->delete();
}
});
UpdateCompanyLedgerWithInvoice::dispatchNow($this->invoice, $this->invoice->amount); UpdateCompanyLedgerWithInvoice::dispatchNow($this->invoice, $this->invoice->amount);
$recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice);