<?php /* * Copyright (c) 2024 LatePoint LLC. All rights reserved. */ class OsInvoicesHelper { public static function get_invoices_for_select() : array { $invoices = new OsInvoiceModel(); $invoices = $invoices->order_by( 'id desc' )->set_limit( 100 )->get_results_as_models(); $invoice_options = []; foreach ( $invoices as $invoice ) { $name = sprintf(esc_html__('%s To %s', 'latepoint'), OsMoneyHelper::format_price($invoice->charge_amount, true, false), $invoice->get_customer()->full_name) . ' [#' . $invoice->get_invoice_number() . ' : ID: ' . $invoice->id . ']'; $invoice_options[] = [ 'value' => $invoice->id, 'label' => esc_html( $name ) ]; } return $invoice_options; } public static function replace_invoice_vars_in_template(string $text, array $vars, string $original_text) : string{ if ( isset( $vars['invoice'] ) ) { $invoice = $vars['invoice']; $needles = [ '{{invoice_status}}', '{{invoice_due_date}}', '{{invoice_amount}}', '{{invoice_number}}', '{{invoice_access_url}}', '{{invoice_pay_url}}', '{{invoice_receipt_url}}', ]; $replacements = [ OsInvoicesHelper::readable_status($invoice->status), $invoice->get_readable_due_at(), OsMoneyHelper::format_price($invoice->charge_amount, true, false), $invoice->get_invoice_number(), $invoice->get_access_url(), $invoice->get_pay_url(), $invoice->get_receipt_url() ]; $text = str_replace( $needles, $replacements, $text ); } return $text; } public static function handle_invoice_created(OsInvoiceModel $invoice){ $objects = []; $objects[] = ['model' => 'invoice', 'id' => $invoice->id, 'model_ready' => $invoice]; OsProcessJobsHelper::create_jobs_for_event('invoice_created', $objects); } public static function handle_invoice_updated(OsInvoiceModel $new_invoice, OsInvoiceModel $old_invoice){ // remove previously scheduled jobs for this invoice because it's changed and might not need them anymore // remove only those that are in "scheduled" status, those that were already sent or errored should stay $jobs = new OsProcessJobModel(); $jobs->delete_where(['status' => LATEPOINT_JOB_STATUS_SCHEDULED, 'object_id' => $new_invoice->id, 'object_model_type' => 'invoice']); $objects = []; $objects[] = ['model' => 'invoice', 'id' => $new_invoice->id, 'model_ready' => $new_invoice]; $objects[] = ['model' => 'old_invoice', 'id' => $old_invoice->id, 'model_ready' => $old_invoice]; OsProcessJobsHelper::create_jobs_for_event('invoice_updated', $objects); } public static function log_invoice_created(OsInvoiceModel $invoice) { $data = []; $data['invoice_id'] = $invoice->id; $data['code'] = 'invoice_created'; $data['description'] = wp_json_encode(['invoice_data_vars' => $invoice->get_data_vars()]); OsActivitiesHelper::create_activity($data); } public static function log_invoice_updated(OsInvoiceModel $invoice, OsInvoiceModel $old_invoice) { $data = []; $data['invoice_id'] = $invoice->id; $data['code'] = 'invoice_updated'; $data['description'] = wp_json_encode(['invoice_data_vars' => ['new' => $invoice->get_data_vars(), 'old' => $old_invoice->get_data_vars()]]); OsActivitiesHelper::create_activity($data); } public static function readable_status( string $status ): string { $statuses = self::list_of_statuses_for_select(); return $statuses[ $status ] ?? __( 'n/a', 'latepoint' ); } public static function get_invoice_by_key( string $key ): OsInvoiceModel { if ( empty( $key ) ) { return new OsInvoiceModel(); } $invoice = new OsInvoiceModel(); return $invoice->where( [ 'access_key' => $key ] )->set_limit( 1 )->get_results_as_models(); } public static function get_invoice_logo_url(): string { $default_logo_url = LATEPOINT_IMAGES_URL . 'logo.svg'; return OsImageHelper::get_image_url_by_id(OsSettingsHelper::get_settings_value('invoices_company_logo'), 'thumbnail', $default_logo_url); } public static function invoice_document_html( OsInvoiceModel $invoice, bool $show_controls, ?OsTransactionModel $transaction = null ) { $invoice_data = json_decode( $invoice->data, true ); ?> <div class="invoice-document status-<?php echo esc_attr( $invoice->status ); ?>" data-invoice-id="<?php echo esc_attr($invoice->id); ?>" data-route="<?php echo OsRouterHelper::build_route_name('invoices', 'change_status'); ?>"> <?php if ( $show_controls ) { ?> <div class="invoice-controls"> <div class="ic-block"> <?php echo OsFormHelper::select_field( 'invoice[status]', __( 'Status', 'latepoint' ), OsInvoicesHelper::list_of_statuses_for_select(), $invoice->status, ['class' => 'invoice-change-status-selector'] ); ?> </div> <div class="ic-block"> <a target="_blank" href="<?php echo $invoice->get_access_url(); ?>" class="ic-external-link"> <span><?php esc_html_e( 'Open', 'latepoint' ); ?></span> <i class="latepoint-icon latepoint-icon-external-link"></i> </a> </div> <div class="ic-block make-last"> <button type="button" class="latepoint-btn latepoint-btn-sm latepoint-btn-outline" data-os-params="<?php echo esc_attr(http_build_query( [ 'invoice_id' => $invoice->id ] )); ?>" data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name( 'invoices', 'edit_data' )); ?>" data-os-output-target="lightbox" data-os-after-call="latepointInvoicesAdminFeature.init_invoice_data_form" data-os-lightbox-classes="width-500"> <i class="latepoint-icon latepoint-icon-edit-2"></i> <span><?php esc_html_e( 'Edit Data', 'latepoint' ); ?></span> </button> <button type="button" class="latepoint-btn latepoint-btn-sm latepoint-btn-outline" data-os-params="<?php echo esc_attr(http_build_query( [ 'invoice_id' => $invoice->id ] )); ?>" data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name( 'invoices', 'email_form' )); ?>" data-os-output-target="lightbox" data-os-after-call="latepointInvoicesAdminFeature.init_email_invoice_form" data-os-lightbox-classes="width-500"> <i class="latepoint-icon latepoint-icon-mail"></i> <span><?php esc_html_e( 'Email Invoice', 'latepoint' ); ?></span> </button> </div> </div> <?php } ?> <div class="invoice-document-i"> <?php if ( empty( $transaction ) ) { switch ( $invoice->status ) { case LATEPOINT_INVOICE_STATUS_PAID: echo '<div class="invoice-status-paid-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>'; break; case LATEPOINT_INVOICE_STATUS_DRAFT: echo '<div class="invoice-status-draft-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>'; break; case LATEPOINT_INVOICE_STATUS_VOID: echo '<div class="invoice-status-voided-label">' . esc_html( self::readable_status( $invoice->status ) ) . '</div>'; break; } } ?> <div class="invoice-heading"> <div class="invoice-info"> <div class="invoice-title"><?php echo $transaction ? esc_html__( 'Receipt', 'latepoint' ) : esc_html__( 'Invoice', 'latepoint' ); ?></div> <div class="invoice-data"> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'Invoice number', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( $invoice->get_invoice_number() ); ?></div> </div> <?php if ( $transaction ) { ?> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'Receipt number', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( $transaction->receipt_number ); ?></div> </div> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'Date paid', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( OsTimeHelper::get_readable_date( new OsWpDateTime( $transaction->created_at, new DateTimeZone('UTC') ) ) ); ?></div> </div> <?php } else { ?> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'Date of issue', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( OsTimeHelper::get_readable_date( new OsWpDateTime( $invoice->created_at, new DateTimeZone('UTC') ) ) ); ?></div> </div> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'Date due', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( OsTimeHelper::get_readable_date( new OsWpDateTime( $invoice->due_at, new DateTimeZone('UTC') ) ) ); ?></div> </div> <?php } ?> <?php if(!empty($invoice_data['tax_id'])){ ?> <div class="invoice-row"> <div class="id-label"><?php esc_html_e( 'VAT Number', 'latepoint' ); ?></div> <div class="id-value"><?php echo esc_html( $invoice_data['tax_id'] ); ?></div> </div> <?php } ?> </div> </div> <div class="invoice-logo"> <img src="<?php echo esc_attr( self::get_invoice_logo_url() ); ?>" width="50" height="50" alt="<?php esc_attr_e('LatePoint Dashboard', 'latepoint'); ?>"> </div> </div> <div class="invoice-to-from"> <div class="invoice-from"> <?php if(!empty($invoice_data['company'])){ ?> <div class="if-heading"><?php echo esc_html( $invoice_data['company'] ); ?></div> <?php } ?> <div class="if-data-block"> <?php echo wp_kses($invoice_data['from'], ['br' => []]); ?> </div> </div> <div class="invoice-from"> <div class="if-heading"><?php echo esc_html( __('Bill to', 'latepoint') ); ?></div> <div class="if-data-block"> <?php echo wp_kses($invoice_data['to'], ['br' => []]); ?> </div> </div> </div> <?php do_action( 'latepoint_invoice_before_amount_info', $invoice, $transaction ); ?> <?php if ( empty( $transaction ) ) { ?> <div class="invoice-due-info"> <div class="invoice-due-amount"> <?php echo esc_html( sprintf( __( '%s due %s', 'latepoint' ), OsMoneyHelper::format_price( $invoice->charge_amount, true, false ), OsTimeHelper::get_readable_date_from_string( $invoice->due_at ) ) ); ?> </div> <?php if ( $invoice->status == LATEPOINT_INVOICE_STATUS_OPEN ) { ?> <div class="invoice-due-pay-link-w"> <a href="<?php echo $invoice->get_pay_url(); ?>" target="_blank"><?php esc_html_e( 'Pay Online', 'latepoint' ); ?></a> </div> <?php } ?> <?php if ( $invoice->status == LATEPOINT_INVOICE_STATUS_PAID || $invoice->get_successful_payments() ) { ?> <div class="invoice-due-pay-link-w"> <a target="_blank" href="<?php echo OsOrdersHelper::generate_direct_manage_order_url( $invoice->get_order(), 'customer', 'list_payments' ) ?>"><?php esc_html_e( 'View Payments', 'latepoint' ); ?></a> </div> <?php } ?> </div> <?php } else { ?> <div class="invoice-due-info"> <div class="invoice-due-amount"> <?php echo esc_html( sprintf( __( '%s paid on %s', 'latepoint' ), OsMoneyHelper::format_price( $transaction->amount, true, false ), OsTimeHelper::get_readable_date_from_string( $transaction->created_at ) ) ); ?> </div> </div> <?php } ?> <div class="invoice-items"> <div class="invoice-items-table-heading"> <div class="it-column"><?php esc_html_e( 'Description', 'latepoint' ); ?></div> <div class="it-column"><?php esc_html_e( 'Amount', 'latepoint' ); ?></div> </div> <?php OsPriceBreakdownHelper::output_price_breakdown( $invoice_data['price_breakdown'] ); ?> </div> <div class="invoice-totals"> <div class="it-row"> <div class="it-column"><?php esc_html_e( 'Subtotal', 'latepoint' ); ?></div> <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $invoice_data['totals']['subtotal'], true, false ) ); ?></div> </div> <div class="it-row"> <div class="it-column"><?php esc_html_e( 'Total', 'latepoint' ); ?></div> <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $invoice_data['totals']['total'], true, false ) ); ?></div> </div> <?php if ( empty( $transaction ) ) { ?> <?php if ( ! empty( $invoice_data['totals']['payments'] ) ) { ?> <div class="it-row it-row-positive"> <div class="it-column"><?php esc_html_e( 'Payments & Credits', 'latepoint' ); ?></div> <div class="it-column"><?php echo '-' . esc_html( OsMoneyHelper::format_price( $invoice_data['totals']['payments'], true, false ) ); ?></div> </div> <?php } ?> <div class="it-row it-row-bold"> <div class="it-column"><?php esc_html_e( 'Amount Due', 'latepoint' ); ?></div> <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $invoice->charge_amount, true, false ) ); ?></div> </div> <?php } else { ?> <div class="it-row it-row-bold"> <div class="it-column"><?php esc_html_e( 'Amount Paid', 'latepoint' ); ?></div> <div class="it-column"><?php echo esc_html( OsMoneyHelper::format_price( $transaction->amount, true, false ) ); ?></div> </div> <?php } ?> </div> <?php if ( OsSettingsHelper::get_settings_value( 'invoice_terms', '' ) ) { ?> <div class="invoice-terms"> <div class="terms-heading"><?php esc_html_e( 'Terms & Conditions', 'latepoint' ); ?></div> <div class="terms-content"><?php echo esc_html( OsSettingsHelper::get_settings_value( 'invoice_terms', '' ) ); ?></div> </div> <?php } ?> </div> </div> <?php } public static function list_of_statuses_for_select(): array { $statuses = [ LATEPOINT_INVOICE_STATUS_OPEN => __( 'Open', 'latepoint' ), LATEPOINT_INVOICE_STATUS_PAID => __( 'Paid', 'latepoint' ), LATEPOINT_INVOICE_STATUS_PARTIALLY_PAID => __( 'Partially Paid', 'latepoint' ), LATEPOINT_INVOICE_STATUS_DRAFT => __( 'Draft', 'latepoint' ), LATEPOINT_INVOICE_STATUS_VOID => __( 'Void', 'latepoint' ), LATEPOINT_INVOICE_STATUS_UNCOLLECTIBLE => __( 'Uncollectible', 'latepoint' ), ]; /** * Get the list of invoice statuses * * @param {array} $statuses Array of invoice statuses * * @returns {array} Filtered array of invoice statuses * @since 5.1.0 * @hook latepoint_invoices_statuses_for_select * */ return apply_filters( 'latepoint_invoices_statuses_for_select', $statuses ); } public static function list_of_payment_portions_for_select(): array { $payment_portions = [ LATEPOINT_PAYMENT_PORTION_FULL => __('Full', 'latepoint'), LATEPOINT_PAYMENT_PORTION_REMAINING => __('Remaining', 'latepoint'), LATEPOINT_PAYMENT_PORTION_DEPOSIT => __('Deposit', 'latepoint'), LATEPOINT_PAYMENT_PORTION_CUSTOM => __('Custom', 'latepoint'), ]; /** * Get the list of invoice payment_portions * * @param {array} $payment_portions Array of invoice payment_portions * * @returns {array} Filtered array of invoice payment_portions * @since 5.1.0 * @hook latepoint_invoices_payment_portions_for_select * */ return apply_filters( 'latepoint_invoices_payment_portions_for_select', $payment_portions ); } /** * @param OsOrderModel $order * * @return void * @throws Exception */ public static function list_invoices_for_order( OsOrderModel $order ) { if ( $order->is_new_record() ) { return; } $invoices = new OsInvoiceModel(); $invoices = $invoices->where( [ 'order_id' => $order->id ] )->get_results_as_models(); if ( OsRolesHelper::can_user( 'invoice__view' ) ) { ?> <div class="invoices-info-w"> <div class="os-form-sub-header"> <h3><?php esc_html_e( 'Invoices', 'latepoint' ); ?></h3> </div> <div class="list-of-invoices"> <?php if ( $invoices ) { foreach ( $invoices as $invoice ) { echo OsInvoicesHelper::generate_invoice_tile_on_order_edit_form($invoice); } } ?> </div> <?php if ( OsRolesHelper::can_user( 'invoice__create' ) ) { ?> <div class="quick-add-item-button" data-os-params="<?php echo OsUtilHelper::build_os_params(['order_id' => $order->id]); ?>" data-os-after-call="latepointInvoicesAdminFeature.init_quick_invoice_settings_form" data-os-output-target=".quick-invoice-settings-form-wrapper" data-os-before-after="before" data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'invoices', 'new_form' ) ); ?>"> <i class="latepoint-icon latepoint-icon-plus2"></i> <span><?php esc_html_e( 'New Invoice', 'latepoint' ); ?></span> </div> <div class="quick-invoice-settings-form-wrapper"></div> <?php } ?> </div> <?php } } /** * @param OsOrderModel $order * * @return array */ public static function generate_invoice_data_from_order( OsOrderModel $order ): array { $data = [ 'company' => OsReplacerHelper::replace_business_vars(OsSettingsHelper::get_settings_value( 'invoices_company_name', '' )), 'from' => OsReplacerHelper::replace_all_vars(OsInvoicesHelper::get_invoice_data_bill_from(), ['order' => $order, 'customer' => $order->get_customer()]), 'to' => OsReplacerHelper::replace_all_vars(OsInvoicesHelper::get_invoice_data_bill_to(), ['order' => $order, 'customer' => $order->get_customer()]), 'price_breakdown' => json_decode( $order->price_breakdown ), 'tax_id' => OsSettingsHelper::get_settings_value('invoices_tax_id', ''), 'totals' => [ 'subtotal' => $order->get_subtotal(), 'total' => $order->get_total(), ] ]; /** * Invoice data generated from an order * * @since 5.1.0 * @hook latepoint_invoices_data_from_order * * @param {array} $data array of values for invoice * @param {OsOrderModel} $order order model * @returns {array} The filtered array of invoice data */ $data = apply_filters('latepoint_invoices_data_from_order', $data, $order); return $data; } /** * @param OsOrderModel $order * @param OsPaymentRequestModel|null $payment_request * * @return void */ public static function create_invoices_for_new_order( OsOrderModel $order, ?OsPaymentRequestModel $payment_request = null) { $existing_invoices = new OsInvoiceModel(); $existing_invoices = $existing_invoices->where(['order_id' => $order->id])->get_results_as_models(); $total_invoiced_amount = 0; if($existing_invoices){ foreach($existing_invoices as $invoice) { $total_invoiced_amount+= $invoice->charge_amount; } } $order_total = $order->get_total(); $total_invoiced_amount = OsMoneyHelper::pad_to_db_format( $total_invoiced_amount ); if($total_invoiced_amount > 0 && $total_invoiced_amount == $order_total){ return; } $invoice = new OsInvoiceModel(); $invoice->order_id = $order->id; $invoice->data = json_encode( self::generate_invoice_data_from_order( $order ) ); if ( $order->get_initial_payment_data_value( 'time' ) == LATEPOINT_PAYMENT_TIME_NOW ) { // need to pay now, portion will depend on what customer/booker selected $invoice->payment_portion = $order->get_initial_payment_data_value( 'portion' ); $invoice->charge_amount = $order->get_initial_payment_data_value( 'charge_amount' ); if ( $order->get_initial_payment_data_value( 'portion' ) != LATEPOINT_PAYMENT_PORTION_FULL ) { // since we are not paying full balance, create invoice for the remaining amount $invoice_for_remaining_balance = clone $invoice; $invoice_for_remaining_balance->charge_amount = $order->get_total() - $invoice->charge_amount; $invoice_for_remaining_balance->payment_portion = LATEPOINT_PAYMENT_PORTION_REMAINING; } if ( ! empty( $payment_request ) ) { $invoice->due_at = $payment_request->due_at; } } else { // will pay later, need to generate full price invoice $invoice->charge_amount = $order->get_total(); $invoice->payment_portion = LATEPOINT_PAYMENT_PORTION_FULL; } if ( $order->get_total_amount_paid_from_transactions() == $invoice->charge_amount ) { // since the order has just been created - any transactions that were made - are part of the time of creation, so should be on the invoice $invoice->status = LATEPOINT_INVOICE_STATUS_PAID; } if ( $invoice->save() ) { /** * Invoice was created * * @param {OsInvoiceModel} $invoice instance of invoice model that was created * * @since 5.1.0 * @hook latepoint_invoice_created * */ do_action( 'latepoint_invoice_created', $invoice ); if ( ! empty( $payment_request ) ) { // if we have payment request, update it with created invoice $payment_request->invoice_id = $invoice->id; $payment_request->order_id = $order->id; if($payment_request->save()){ /** * Invoice was created * * @param {OsInvoiceModel} $payment_request instance of payment request model that was created * * @since 5.1.0 * @hook latepoint_payment_request_created * */ do_action( 'latepoint_payment_request_created', $payment_request ); } } if ( isset( $invoice_for_remaining_balance ) && $invoice_for_remaining_balance->charge_amount > 0 ) { $order_items = $order->get_items(); foreach ( $order_items as $item ) { if ( $item->is_booking() ) { $booking = $item->build_original_object_from_item_data(); $invoice_for_remaining_balance->due_at = $booking->start_datetime_utc; break; } } $invoice_for_remaining_balance->status = LATEPOINT_INVOICE_STATUS_DRAFT; $invoice_for_remaining_balance->save(); /** * Invoice was created * * @param {OsInvoiceModel} $invoice instance of invoice model that was created * * @since 5.1.0 * @hook latepoint_invoice_created * */ do_action( 'latepoint_invoice_created', $invoice_for_remaining_balance ); } } } /** * * Tries to get a matching invoice for a transaction, this is only useful when a new order is created and a transaction needs to find an invoice to attach to * * @param OsTransactionModel $transaction * * @return OsInvoiceModel */ public static function get_matching_invoice_for_transaction( OsTransactionModel $transaction ): OsInvoiceModel { $invoice = new OsInvoiceModel(); $invoice->where( [ 'order_id' => $transaction->order_id ] ); $invoice->where( [ 'payment_portion' => $transaction->payment_portion ] ); $invoice->where( [ 'charge_amount' => $transaction->amount ] ); $invoice->where( [ 'status' => LATEPOINT_INVOICE_STATUS_OPEN ] ); $invoice = $invoice->set_limit( 1 )->get_results_as_models(); if ( empty( $invoice ) ) { $invoice = new OsInvoiceModel(); } /** * Try to get a matching invoice for a transaction * * @param {OsTransactionModel} $transaction transaction to match invoice to * * @returns {OsInvoiceModel} Filtered invoice model * @since 5.1.0 * @hook latepoint_invoice_get_matching_invoice_for_transaction * */ return apply_filters( 'latepoint_invoice_get_matching_invoice_for_transaction', $invoice, $transaction ); } public static function create_invoice_from_transaction( OsTransactionModel $transaction_to_create_invoice ): bool { $order = new OsOrderModel( $transaction_to_create_invoice->order_id ); if ( $order->is_new_record() ) { return false; } $invoice = new OsInvoiceModel(); $invoice->charge_amount = $transaction_to_create_invoice->amount; $invoice->data = self::generate_invoice_data_from_order( $order ); $invoice->order_id = $order->id; $invoice->status = LATEPOINT_INVOICE_STATUS_PAID; $invoice->payment_portion = $transaction_to_create_invoice->payment_portion; $invoice->due_at = $transaction_to_create_invoice->created_at; if ( $invoice->save() ) { return $transaction_to_create_invoice->update_attributes( [ 'invoice_id' => $invoice->id ] ); } return false; } public static function generate_invoice_tile_on_order_edit_form( $invoice ) : string { $html = ''; $html.= '<div class="os-invoice-wrapper" data-reload-tile-route="'.esc_attr(OsRouterHelper::build_route_name('invoices', 'reload_invoice_tile')).'" data-route="' . esc_attr( OsRouterHelper::build_route_name( 'invoices', 'view' ) ) . '" data-invoice-id="' . esc_attr( $invoice->id ) . '">'; $html.= '<div class="quick-invoice-head"> <div class="quick-invoice-icon"><i class="latepoint-icon latepoint-icon-file-text"></i></div> <div class="quick-invoice-amount">' . OsMoneyHelper::format_price( $invoice->charge_amount, true, false ) . '</div> <div class="lp-invoice-status lp-invoice-status-' . $invoice->status . '">' . self::readable_status( $invoice->status ) . '</div> </div> <div class="quick-invoice-sub"> <div class="lp-invoice-number"><span>' . esc_html__( 'Invoice Number:', 'latepoint' ) . '</span> <strong>' . esc_html( $invoice->get_invoice_number() ) . '</strong></div> <div class="lp-invoice-date">' . sprintf( esc_html__( 'Due: %s', 'latepoint' ), OsTimeHelper::get_readable_date( new OsWpDateTime( $invoice->due_at, new DateTimeZone('UTC') ) ) ) . '</div> </div>'; $html.= '</div>'; return $html; } public static function update_activity_after_invoice_job_run( OsActivityModel $activity, OsProcessJobModel $process_job, string $event_type ): OsActivityModel { switch ( $event_type ) { case 'invoice_created': case 'invoice_updated': $invoice = new OsInvoiceModel( $process_job->object_id ); $activity->customer_id = $invoice->get_customer()->id; $activity->order_id = $invoice->order_id; break; } return $activity; } public static function add_activity_view_vars_for_invoice( array $vars, OsActivityModel $activity ): array { $data = json_decode( $activity->description, true ); switch ( $activity->code ) { case 'invoice_created': $link_to_invoice = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>'; $vars['name'] = __('Invoice Created', 'latepoint'); $vars['meta_html'] = '<div class="activity-preview-to"><span class="os-value">' . $link_to_invoice . '</span><span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</div>'; $vars['content_html'] = '<pre class="format-json">' . wp_json_encode( $data['invoice_data_vars'], JSON_PRETTY_PRINT ) . '</pre>'; break; case 'invoice_updated': $link_to_invoice = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>'; $vars['name'] = __('Invoice Updated', 'latepoint'); $vars['meta_html'] = '<div class="activity-preview-to"><span class="os-value">' . $link_to_invoice . '</span><span class="os-label">' . __( 'Updated On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</div>'; $changes = OsUtilHelper::compare_model_data_vars( $data['invoice_data_vars']['new'], $data['invoice_data_vars']['old'] ); $vars['content_html'] = '<pre class="format-json">' . wp_json_encode( $changes, JSON_PRETTY_PRINT ) . '</pre>'; break; } return $vars; } public static function add_invoice_activity_code( array $codes ): array { $codes['invoice_created'] = __( 'Invoice Created', 'latepoint' ); $codes['invoice_updated'] = __( 'Invoice Updated', 'latepoint' ); return $codes; } public static function add_invoice_templates_for_event_actions( array $templates, string $action_type, $wp_filesystem ) { switch ( $action_type ) { case 'send_email': $templates[] = [ 'id' => 'invoice__created__to_customer', 'to_user_type' => 'customer', 'name' => "New Invoice", 'to_email' => '{{customer_full_name}} <{{customer_email}}>', 'subject' => "New Invoice {{invoice_number}}", 'content' => OsEmailHelper::get_email_layout( $wp_filesystem->get_contents( LATEPOINT_ADDON_PRO_VIEWS_ABSPATH . 'mailers/customer/invoice_created.html' ) ) ]; $templates[] = [ 'id' => 'invoice__paid__to_customer', 'to_user_type' => 'customer', 'name' => "Invoice Paid", 'to_email' => '{{customer_full_name}} <{{customer_email}}>', 'subject' => "Invoice #{{invoice_number}} Paid", 'content' => OsEmailHelper::get_email_layout( $wp_filesystem->get_contents( LATEPOINT_ADDON_PRO_VIEWS_ABSPATH . 'mailers/customer/invoice_paid.html' ) ) ]; } return $templates; } public static function get_invoice_object_for_process( array $object_data, string $source, string $value, bool $include_model ): array { switch ( $source ) { case 'invoice_id': $object_data = [ 'model' => 'invoice', 'id' => $value ]; if ( $include_model ) { $model = new OsInvoiceModel( $value ); $object_data['model_ready'] = $model; } break; } return $object_data; } public static function prepare_replacement_vars_for_invoice( array $vars, array $data_objects, array $other_vars ): array { foreach ( $data_objects as $data_object ) { switch ( $data_object['model'] ) { case 'old_invoice': $old_invoice = $data_object['model_ready'] ?? new OsInvoiceModel( $data_object['id'] ); $temp_vars = self::generate_replacement_vars_from_invoice( $old_invoice ); foreach ( $temp_vars as $key => $data ) { $vars[ 'old_' . $key ] = $data; } break; case 'invoice': $invoice = $data_object['model_ready'] ?? new OsInvoiceModel( $data_object['id'] ); $vars = array_merge( $vars, self::generate_replacement_vars_from_invoice( $invoice ) ); break; } } return $vars; } /** * * Prepares an array of variables to be used in replacer method from a order object * * @param OsInvoiceModel $invoice * @param array $other_vars * * @return array */ public static function generate_replacement_vars_from_invoice( OsInvoiceModel $invoice, array $other_vars = [] ): array { $vars = []; $vars['invoice'] = $invoice; $vars['customer'] = $invoice->get_customer(); $vars['order'] = $invoice->get_order(); if ( ! empty( $other_vars ) ) { $vars = array_merge( $vars, $other_vars ); } /** * Returns an array of replacement variables, based on supplied <code>OsInvoiceModel</code> instance * * @param {array} $vars Current array of replacement variables * @param {OsInvoiceModel} $invoice Instance of <code>OsInvoiceModel</code> to generate replacement variables for * @param {array} $other_vars Array of additional (pre-prepared) replacement variables * * @returns {array} Filtered array of replacement variables * @since 1.1.0 * @hook latepoint_prepare_replacement_vars_from_invoice * */ return apply_filters( 'latepoint_prepare_replacement_vars_from_invoice', $vars, $invoice, $other_vars ); } public static function add_data_source_for_invoice_events( array $data_sources, \LatePoint\Misc\ProcessEvent $event ): array { switch ( $event->type ) { case 'invoice_created': $invoices_for_select = \OsInvoicesHelper::get_invoices_for_select(); $data_sources[] = [ 'name' => 'invoice_id', 'values' => $invoices_for_select, 'label' => __( 'Choose an invoice for this test run:', 'latepoint' ), 'model' => 'invoice' ]; break; case 'invoice_updated': $invoices_for_select = \OsInvoicesHelper::get_invoices_for_select(); $data_sources[] = [ 'name' => 'new_invoice_id', 'values' => $invoices_for_select, 'label' => __( 'Choose old invoice to be used for this test run:', 'latepoint' ), 'model' => 'invoice' ]; $data_sources[] = [ 'name' => 'old_invoice_id', 'values' => $invoices_for_select, 'label' => __( 'Choose new invoice to be used for this test run:', 'latepoint' ), 'model' => 'invoice' ]; break; } return $data_sources; } public static function prepare_data_for_run( \LatePoint\Misc\ProcessAction $action ): \LatePoint\Misc\ProcessAction { foreach ( $action->selected_data_objects as $data_object ) { switch ( $data_object['model'] ) { case 'invoice': $action->prepared_data_for_run['activity_data']['invoice_id'] = $data_object['id']; if ( ! empty( $data_object['model_ready'] ) ) { $action->prepared_data_for_run['activity_data']['order_id'] = $data_object['model_ready']->order_id; } break; } } return $action; } public static function set_object_model_type_for_invoice_processes( ?string $object_model_type, OsProcessModel $process, array $objects ): ?string { if ( in_array( $process->event_type, [ 'invoice_created', 'invoice_updated' ] ) ) { $object_model_type = 'invoice'; } return $object_model_type; } public static function set_event_time_utc_for_invoice_processes( ?OsWpDateTime $event_time_utc, OsProcessModel $process, array $objects ): ?OsWpDateTime { if ( in_array( $process->event_type, [ 'invoice_created', 'invoice_updated' ] ) ) { try { switch ( $process->event_type ) { case 'invoice_created': $event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->created_at, new DateTimeZone( 'UTC' ) ); break; case 'invoice_updated': $event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->updated_at, new DateTimeZone( 'UTC' ) ); break; } } catch ( Exception $e ) { OsDebugHelper::log( 'Error creating jobs for workflow', 'process_jobs_error', print_r( $process->id, true ) . ' ' . print_r( $objects, true ) . ' ' . $e->getMessage() ); return $event_time_utc; } } return $event_time_utc; } public static function add_invoice_to_process_event_models( OsModel $model, string $property_object_name ): OsModel { switch ( $property_object_name ) { case 'old_invoice': case 'invoice': $model = new OsInvoiceModel(); break; } return $model; } public static function add_values_for_process_event_condition( array $values, string $property, string $property_object, string $property_attribute ): array { switch ( $property_object ) { case 'invoice': case 'old_invoice': switch ( $property_attribute ) { case 'status': $statuses = OsInvoicesHelper::list_of_statuses_for_select(); foreach ( $statuses as $status_key => $status ) { $values[] = [ 'value' => $status_key, 'label' => $status ]; } break; case 'payment_portion': $statuses = OsInvoicesHelper::list_of_payment_portions_for_select(); foreach ( $statuses as $status_key => $status ) { $values[] = [ 'value' => $status_key, 'label' => $status ]; } break; } break; } return $values; } public static function add_operators_to_conditions_for_invoice_events( array $operators, string $property, string $object_code, string $object_property ): array { switch ( $object_code ) { case 'old_invoice': // TODO time range operators instead of removing these opearators completely if ( $object_property != 'due_at' ) { $operators['equal'] = __( 'was equal to', 'latepoint' ); $operators['not_equal'] = __( 'was not equal to', 'latepoint' ); } $operators['changed'] = __( 'has changed', 'latepoint' ); $operators['not_changed'] = __( 'has not changed', 'latepoint' ); break; case 'invoice': // TODO time range operators instead of removing these opearators completely if ( $object_property != 'due_at' ) { $operators['equal'] = __( 'is equal to', 'latepoint' ); $operators['not_equal'] = __( 'is not equal to', 'latepoint' ); } break; } return $operators; } public static function add_conditions_for_invoice_events( array $objects, string $event_type ): array { switch ( $event_type ) { case 'invoice_created': $objects[] = [ 'code' => 'invoice', 'model' => 'OsInvoiceModel', 'label' => __( 'Invoice', 'latepoint' ), 'properties' => [] ]; break; case 'invoice_updated': $objects[] = [ 'code' => 'old_invoice', 'model' => 'OsInvoiceModel', 'label' => __( 'Old Invoice', 'latepoint' ), 'properties' => [] ]; $objects[] = [ 'code' => 'invoice', 'model' => 'OsInvoiceModel', 'label' => __( 'New Invoice', 'latepoint' ), 'properties' => [] ]; break; } return $objects; } public static function add_process_events_for_invoices( array $process_events ): array { $process_events[] = 'invoice_created'; $process_events[] = 'invoice_updated'; return $process_events; } public static function add_names_for_process_events_for_invoices( array $process_event_names ): array { $process_event_names['invoice_created'] = __( 'Invoice Created', 'latepoint' ); $process_event_names['invoice_updated'] = __( 'Invoice Updated', 'latepoint' ); return $process_event_names; } public static function get_subject_for_invoice_email(): string { $default_subject = 'Your Invoice for Order #{{order_confirmation_code}}'; return OsSettingsHelper::get_settings_value( 'invoices_email_subject', $default_subject ); } public static function get_invoice_data_bill_from(): string { $default = '{{business_address}}<br>{{business_phone}}'; return OsSettingsHelper::get_settings_value( 'invoices_data_from', $default ); } public static function get_invoice_data_bill_to(): string { $default = '{{customer_full_name}}<br>{{customer_email}}<br>{{customer_phone}}'; return OsSettingsHelper::get_settings_value( 'invoices_data_to', $default ); } public static function get_content_for_invoice_email(): string { $default_content = 'Hi {{customer_full_name}},<br><br>Thank you for choosing {{business_name}}.<br>You can view your invoice using <a href="{{invoice_access_url}}">this link</a><br><br>If you have any questions, feel free to contact us.'; return OsSettingsHelper::get_settings_value( 'invoices_email_content', $default_content ); } public static function output_invoice_vars() { ?> <div class="available-vars-block"> <h4><?php _e( 'Invoices', 'latepoint' ); ?></h4> <ul> <li><span class="var-label"><?php esc_html_e( 'Invoice Status', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_status}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Due Date', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_due_date}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Amount', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_amount}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Number', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_number}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Access URL', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_access_url}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Pay URL', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_pay_url}}</span></li> <li><span class="var-label"><?php esc_html_e( 'Invoice Receipt URL', 'latepoint' ); ?></span> <span class="var-code os-click-to-copy">{{invoice_receipt_url}}</span></li> </ul> </div> <?php } public static function add_settings_sticky_menu_item(array $items) : array{ $items[] = ['href' => 'stickySectionInvoice', 'label' => __( 'Invoices', 'latepoint' )]; return $items; } public static function output_invoice_settings() { ?> <div class="white-box section-anchor" id="stickySectionInvoice"> <div class="white-box-header"> <div class="os-form-sub-header"><h3><?php esc_html_e( 'Invoices', 'latepoint' ); ?></h3></div> </div> <div class="white-box-content no-padding"> <?php echo '<div class="sub-section-row"> <div class="sub-section-label"> <h3>' . __( 'Invoice Data', 'latepoint' ) . '</h3> </div> <div class="sub-section-content">'; echo '<div class="os-row"> <div class="os-col-lg-12">'.OsFormHelper::media_uploader_field( 'settings[invoices_company_logo]', 0, __( 'Company Logo', 'latepoint' ), __( 'Remove Image', 'latepoint' ), OsSettingsHelper::get_settings_value( 'invoices_company_logo' ) ).'</div> </div>'; echo '<div class="os-row os-mb-2">'; echo '<div class="os-col-4">'; echo OsFormHelper::text_field( 'settings[invoices_company_name]', __( 'Company Name', 'latepoint' ), OsSettingsHelper::get_settings_value( 'invoices_company_name', '' ), [ 'theme' => 'simple' ] ); echo '</div>'; echo '<div class="os-col-4">'; echo OsFormHelper::text_field( 'settings[invoices_tax_id]', __( 'VAT Number/Tax ID', 'latepoint' ), OsSettingsHelper::get_settings_value( 'invoices_tax_id', '' ), [ 'theme' => 'simple' ] ); echo '</div>'; echo '<div class="os-col-4">'; echo OsFormHelper::text_field( 'settings[invoices_number_prefix]', __( 'Number Prefix', 'latepoint' ), OsSettingsHelper::get_settings_value( 'invoices_number_prefix', 'INV-' ), [ 'theme' => 'simple' ] ); echo '</div>'; echo '</div>'; echo '<div class="os-mb-2">'; echo OsFormHelper::textarea_field( 'settings[invoices_data_from]', __( 'Bill From', 'latepoint' ), self::get_invoice_data_bill_from(), [ 'theme' => 'simple' ] ); echo '</div>'; echo '<div>'; echo OsFormHelper::textarea_field( 'settings[invoices_data_to]', __( 'Bill To', 'latepoint' ), self::get_invoice_data_bill_to(), [ 'theme' => 'simple' ] ); echo '</div>'; echo '</div> </div> <div class="sub-section-row"> <div class="sub-section-label"> <h3>' . __( 'Email Invoice', 'latepoint' ) . '</h3> </div> <div class="sub-section-content"> <div class="latepoint-message latepoint-message-subtle">' . __( 'This subject and content will be used when invoice is being emailed. ', 'latepoint' ) . OsUtilHelper::template_variables_link_html() . '</div>'; echo OsFormHelper::text_field( 'settings[invoices_email_subject]', __( 'Subject', 'latepoint' ), self::get_subject_for_invoice_email(), [ 'theme' => 'simple' ] ); OsFormHelper::wp_editor_field( 'settings[invoices_email_content]', 'settingsInvoiceEmailContent', __( 'Email Content', 'latepoint' ), self::get_content_for_invoice_email() ); echo '</div> </div>'; ?> </div> </div> <?php } /** * Get Invoice by transaction intent key * @param string $intent_key * @return OsInvoiceModel | false */ public static function get_invoice_by_transaction_intent_key( string $intent_key ) { $transaction_intent = OsTransactionIntentHelper::get_transaction_intent_by_intent_key($intent_key); if (!$transaction_intent->is_new_record()) { return new OsInvoiceModel($transaction_intent->invoice_id); } return false; } }