AbstractAbility.php
2 weeks ago
ArchiveProduct.php
4 months ago
CancelSubscription.php
4 months ago
CreateCoupon.php
4 months ago
CreateCustomer.php
4 months ago
CreateFulfillment.php
4 months ago
CreateInvoice.php
2 months ago
CreatePrice.php
2 months ago
CreateProduct.php
4 months ago
CreatePromotion.php
4 months ago
CreateRefund.php
2 months ago
DeleteCoupon.php
4 months ago
DeleteCustomer.php
4 months ago
DeleteFulfillment.php
4 months ago
DeletePromotion.php
4 months ago
DuplicateProduct.php
4 months ago
FilterCustomers.php
2 months ago
FilterOrders.php
2 months ago
FilterSubscriptions.php
2 months ago
GetAbandonedCheckout.php
3 months ago
GetCoupon.php
4 months ago
GetCustomer.php
4 months ago
GetCustomersFilterSchema.php
2 months ago
GetFulfillment.php
4 months ago
GetFulfillmentItem.php
4 months ago
GetLicense.php
4 months ago
GetOrder.php
4 months ago
GetOrderStatistics.php
4 months ago
GetOrdersFilterSchema.php
2 months ago
GetProduct.php
4 months ago
GetPromotion.php
4 months ago
GetRefund.php
4 months ago
GetStoreDashboard.php
4 months ago
GetStoreInfo.php
4 months ago
GetSubscription.php
4 months ago
GetSubscriptionsFilterSchema.php
2 months ago
ListAbandonedCheckouts.php
3 months ago
ListCoupons.php
4 months ago
ListCustomers.php
4 months ago
ListFulfillments.php
4 months ago
ListLicenses.php
4 months ago
ListOrders.php
4 months ago
ListPrices.php
4 months ago
ListProducts.php
4 months ago
ListPromotions.php
4 months ago
ListRefunds.php
4 months ago
ListSubscriptions.php
4 months ago
UpdateCoupon.php
4 months ago
UpdateCustomer.php
4 months ago
UpdateFulfillment.php
4 months ago
UpdateInvoice.php
4 months ago
UpdatePrice.php
4 months ago
UpdateProduct.php
4 months ago
UpdatePromotion.php
4 months ago
UpdateSubscriptionRenewalDate.php
4 months ago
CreateInvoice.php
275 lines
| 1 | <?php |
| 2 | |
| 3 | namespace SureCart\Abilities\Abilities; |
| 4 | |
| 5 | use SureCart\Models\Checkout; |
| 6 | use SureCart\Models\Invoice; |
| 7 | use SureCart\Models\LineItem; |
| 8 | |
| 9 | /** |
| 10 | * Create a new invoice for a customer. |
| 11 | */ |
| 12 | class CreateInvoice extends AbstractAbility { |
| 13 | |
| 14 | /** |
| 15 | * {@inheritDoc} |
| 16 | */ |
| 17 | public function get_name(): string { |
| 18 | return 'surecart/create-invoice'; |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * {@inheritDoc} |
| 23 | */ |
| 24 | public function get_label(): string { |
| 25 | return __( 'Create Invoice', 'surecart' ); |
| 26 | } |
| 27 | |
| 28 | /** |
| 29 | * {@inheritDoc} |
| 30 | */ |
| 31 | public function get_description(): string { |
| 32 | return __( 'Create a new SureCart invoice for a customer. Requires a customer ID, a price ID for the line item, and a due date. For multi-variant products, a variant_id is required to specify which variant to invoice. Optionally set a custom ad-hoc amount, quantity, and a memo/description. The invoice is created in draft status and then opened so the customer can pay.', 'surecart' ); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * {@inheritDoc} |
| 37 | */ |
| 38 | public function get_annotations(): array { |
| 39 | return array( |
| 40 | 'readonly' => false, |
| 41 | 'destructive' => false, |
| 42 | 'idempotent' => false, |
| 43 | ); |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * {@inheritDoc} |
| 48 | */ |
| 49 | public function get_instructions(): string { |
| 50 | return 'This ability creates a full invoice in one step. It requires a customer_id and a price_id (use surecart/list-prices to find one). For products with multiple variants, you MUST also provide a variant_id — use surecart/get-product to retrieve the product and inspect its variants array to find the correct variant ID. Without a variant_id on multi-variant products, the invoice will fail with "Item no longer available". If the price has ad_hoc enabled, you can override the amount with ad_hoc_amount in smallest currency unit (e.g., 9999 for $99.99). A due_date in YYYY-MM-DD format is required. The invoice is created as a draft, populated with the line item and customer, then opened automatically. Each call creates a new invoice — do not call multiple times for the same invoice. Always confirm the details with the user before creating.'; |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * {@inheritDoc} |
| 55 | */ |
| 56 | public function check_permission(): bool { |
| 57 | return current_user_can( 'publish_sc_invoices' ); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * {@inheritDoc} |
| 62 | */ |
| 63 | public function get_input_schema(): array { |
| 64 | return array( |
| 65 | 'type' => 'object', |
| 66 | 'properties' => array( |
| 67 | 'customer_id' => array( |
| 68 | 'type' => 'string', |
| 69 | 'description' => __( 'The SureCart customer ID to invoice.', 'surecart' ), |
| 70 | ), |
| 71 | 'price_id' => array( |
| 72 | 'type' => 'string', |
| 73 | 'description' => __( 'The price ID to add as a line item. Use surecart/list-prices to find available prices.', 'surecart' ), |
| 74 | ), |
| 75 | 'variant_id' => array( |
| 76 | 'type' => 'string', |
| 77 | 'description' => __( 'The variant ID for multi-variant products. Required when the product has variants. Use surecart/get-product to see available variants and their IDs.', 'surecart' ), |
| 78 | ), |
| 79 | 'due_date' => array( |
| 80 | 'type' => 'string', |
| 81 | 'description' => __( 'Due date in YYYY-MM-DD format.', 'surecart' ), |
| 82 | ), |
| 83 | 'ad_hoc_amount' => array( |
| 84 | 'type' => 'integer', |
| 85 | 'description' => __( 'Custom amount in smallest currency unit (e.g., 9999 for $99.99). Only works with ad-hoc enabled prices.', 'surecart' ), |
| 86 | ), |
| 87 | 'quantity' => array( |
| 88 | 'type' => 'integer', |
| 89 | 'description' => __( 'Line item quantity. Defaults to 1.', 'surecart' ), |
| 90 | 'default' => 1, |
| 91 | ), |
| 92 | 'description' => array( |
| 93 | 'type' => 'string', |
| 94 | 'description' => __( 'Invoice memo or description.', 'surecart' ), |
| 95 | ), |
| 96 | 'notifications_enabled' => array( |
| 97 | 'type' => 'boolean', |
| 98 | 'description' => __( 'Whether to send an email notification to the customer when the invoice is opened. Defaults to true.', 'surecart' ), |
| 99 | 'default' => true, |
| 100 | ), |
| 101 | ), |
| 102 | 'required' => array( 'customer_id', 'price_id', 'due_date' ), |
| 103 | ); |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * {@inheritDoc} |
| 108 | */ |
| 109 | public function get_output_schema(): array { |
| 110 | return array( |
| 111 | 'type' => 'object', |
| 112 | 'properties' => array( |
| 113 | 'success' => array( 'type' => 'boolean' ), |
| 114 | 'invoice' => array( 'type' => 'object' ), |
| 115 | ), |
| 116 | ); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * {@inheritDoc} |
| 121 | * |
| 122 | * @param array $input The input data. |
| 123 | */ |
| 124 | public function execute( array $input ) { |
| 125 | $customer_id = sanitize_text_field( $input['customer_id'] ?? '' ); |
| 126 | if ( empty( $customer_id ) ) { |
| 127 | return $this->error( 'missing_customer_id', __( 'A customer ID is required.', 'surecart' ) ); |
| 128 | } |
| 129 | |
| 130 | $price_id = sanitize_text_field( $input['price_id'] ?? '' ); |
| 131 | if ( empty( $price_id ) ) { |
| 132 | return $this->error( 'missing_price_id', __( 'A price ID is required.', 'surecart' ) ); |
| 133 | } |
| 134 | |
| 135 | $due_date = sanitize_text_field( $input['due_date'] ?? '' ); |
| 136 | if ( empty( $due_date ) ) { |
| 137 | return $this->error( 'missing_due_date', __( 'A due date is required.', 'surecart' ) ); |
| 138 | } |
| 139 | |
| 140 | if ( ! $this->is_valid_date( $due_date ) ) { |
| 141 | return $this->error( 'invalid_date', __( 'due_date must be in YYYY-MM-DD format.', 'surecart' ) ); |
| 142 | } |
| 143 | |
| 144 | $quantity = absint( $input['quantity'] ?? 1 ); |
| 145 | if ( $quantity < 1 ) { |
| 146 | $quantity = 1; |
| 147 | } |
| 148 | |
| 149 | // Step 1: Create a draft invoice. |
| 150 | // Do not hardcode live_mode — let SureCart inherit the store's active mode. |
| 151 | // This ensures invoices can be created for both live and test mode customers. |
| 152 | $invoice = Invoice::create( array() ); |
| 153 | |
| 154 | if ( is_wp_error( $invoice ) ) { |
| 155 | return $invoice; |
| 156 | } |
| 157 | |
| 158 | $checkout_id = $invoice->checkout ?? ''; |
| 159 | if ( is_object( $checkout_id ) ) { |
| 160 | $checkout_id = $checkout_id->id ?? ''; |
| 161 | } |
| 162 | |
| 163 | if ( empty( $checkout_id ) ) { |
| 164 | return $this->error( 'no_checkout', __( 'Invoice was created but no associated checkout was found.', 'surecart' ) ); |
| 165 | } |
| 166 | |
| 167 | // Step 2: Update the checkout with the customer. |
| 168 | // Expand customer.shipping_address so we can sync it to the checkout (mirrors admin UI behavior). |
| 169 | $checkout = Checkout::with( |
| 170 | array( |
| 171 | 'customer', |
| 172 | 'customer.shipping_address', |
| 173 | ) |
| 174 | )->update( |
| 175 | array( |
| 176 | 'id' => $checkout_id, |
| 177 | 'customer_id' => $customer_id, |
| 178 | ) |
| 179 | ); |
| 180 | |
| 181 | if ( is_wp_error( $checkout ) ) { |
| 182 | return $checkout; |
| 183 | } |
| 184 | |
| 185 | // Step 2b: Sync the customer's shipping address to the checkout. |
| 186 | // The admin UI does this as a second PATCH after attaching the customer. |
| 187 | // Without this, open() fails with "Please enter additional shipping address information". |
| 188 | if ( ! empty( $checkout->customer->shipping_address ) ) { |
| 189 | $shipping_address = $checkout->customer->shipping_address; |
| 190 | |
| 191 | // Convert the address model/object to an array for the update. |
| 192 | $address_data = is_object( $shipping_address ) ? (array) $shipping_address : $shipping_address; |
| 193 | |
| 194 | $checkout = Checkout::update( |
| 195 | array( |
| 196 | 'id' => $checkout_id, |
| 197 | 'customer_id' => $customer_id, |
| 198 | 'shipping_address' => $address_data, |
| 199 | ) |
| 200 | ); |
| 201 | |
| 202 | if ( is_wp_error( $checkout ) ) { |
| 203 | return $checkout; |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | // Step 3: Add a line item to the checkout. |
| 208 | $line_item_data = array( |
| 209 | 'checkout' => $checkout_id, |
| 210 | 'price' => $price_id, |
| 211 | 'quantity' => $quantity, |
| 212 | ); |
| 213 | |
| 214 | if ( ! empty( $input['variant_id'] ) ) { |
| 215 | $line_item_data['variant'] = sanitize_text_field( $input['variant_id'] ); |
| 216 | } |
| 217 | |
| 218 | if ( isset( $input['ad_hoc_amount'] ) ) { |
| 219 | $ad_hoc_amount = intval( $input['ad_hoc_amount'] ); |
| 220 | if ( $ad_hoc_amount < 0 ) { |
| 221 | return $this->error( 'invalid_ad_hoc_amount', __( 'ad_hoc_amount must be zero or greater. Negative invoice amounts are not supported.', 'surecart' ) ); |
| 222 | } |
| 223 | $line_item_data['ad_hoc_amount'] = $ad_hoc_amount; |
| 224 | } |
| 225 | |
| 226 | $line_item = LineItem::create( $line_item_data ); |
| 227 | |
| 228 | if ( is_wp_error( $line_item ) ) { |
| 229 | return $line_item; |
| 230 | } |
| 231 | |
| 232 | // Step 4: Update the invoice with due date and memo. |
| 233 | $invoice_update_data = array( |
| 234 | 'id' => $invoice->id, |
| 235 | 'due_date' => ( new \DateTime( $due_date ) )->getTimestamp(), |
| 236 | ); |
| 237 | |
| 238 | if ( ! empty( $input['description'] ) ) { |
| 239 | $invoice_update_data['memo'] = sanitize_textarea_field( $input['description'] ); |
| 240 | } |
| 241 | |
| 242 | // Persist notifications_enabled before opening so the open action respects it. |
| 243 | if ( isset( $input['notifications_enabled'] ) ) { |
| 244 | $invoice_update_data['notifications_enabled'] = (bool) $input['notifications_enabled']; |
| 245 | } |
| 246 | |
| 247 | $updated_invoice = Invoice::update( $invoice_update_data ); |
| 248 | |
| 249 | if ( is_wp_error( $updated_invoice ) ) { |
| 250 | return $updated_invoice; |
| 251 | } |
| 252 | |
| 253 | // Step 5: Open the invoice so the customer can pay. |
| 254 | $opened_invoice = Invoice::with( |
| 255 | array( |
| 256 | 'checkout', |
| 257 | 'checkout.line_items', |
| 258 | 'line_item.price', |
| 259 | 'price.product', |
| 260 | 'checkout.customer', |
| 261 | ) |
| 262 | )->open( $updated_invoice->id ); |
| 263 | |
| 264 | if ( is_wp_error( $opened_invoice ) ) { |
| 265 | return $opened_invoice; |
| 266 | } |
| 267 | |
| 268 | return $this->success( |
| 269 | array( |
| 270 | 'invoice' => $this->model_to_array( $opened_invoice ), |
| 271 | ) |
| 272 | ); |
| 273 | } |
| 274 | } |
| 275 |