bojanz has finished the initial version of the architecture plan for 8, and we can start building. This issue will contain the overall plan, as well as any child issues.

PLAN

The goal of Commerce POS is to allow Commerce to be used as a Point of Sale system. The same installation can be also be used for non-POS purposes, sharing the same orders, products, customers, stock.

The port from D7 to D8 will include a big reduction in the amount of modules, matching the same change that happened in Commerce. Gone are at least the Location, Register, Cashier, UPC Scan, Messages, Payment, Terminal modules.

The D7 version contained a number of abstractions (transactions, terminal plugins) built on top of Commerce that should be removed in favor of native Commerce functionality.

Base module

In D7 the module used a commerce_pos_transaction entity type which referenced a register, a cashier, and an order ID. For D8 I suggest abandoning this layer, and using a custom order type instead (“terminal”) which has a "register" entity_reference (the location is the store ID, and the cashier is the UID). This order type would have a custom workflow (draft - completed - parked - voided - returned). Returns would always start from an original order, not from just a product like today.

This makes the entire POS UI a custom order admin UI, which makes a lot of sense conceptually.

Commerce note: We need to add an Order Type condition to Commerce, this would allow payment gateways to be limited to POS only, when needed.

Payments

In D7 payments are handled via two modules: commerce_pos_payment, commerce_pos_terminal.

The commerce_pos_payment module contains preconfigured offline payment methods.
The commerce_pos_terminal module provides an API for integrating payment terminals in an OO manner, introduced in #2746605: Add support for payment terminal plugins. The main API that integrates with commerce_pos_terminal is commerce_paymenttree. The payment tree API blocks while waiting for the customer to do the payment on the terminal, so commerce_pos_terminal integrates with the background_process module to perform this in an async manner. Meanwhile, the payment terminal form polls the queue for results while showing a “Please wait” modal.

Commerce 2.x contains “manual payment gateways”, which replace the commerce_cop contrib from D7. The entire commerce_pos_payment module can be replaced with default config in config/install (a payment gateway for each of the Cash/Credit/Debit/Gift card options).

The 2.x payment gateway API is similar to the D7 payment terminal plugins, in the sense that we have a class with purchase/void/refund methods that wrap the actual SDK. Thus, most of commerce_pos_terminal is no longer needed. I advise against trying to shoehorn terminal gateways into on-site or off-site gateways. Instead, POS should define its own gateway category (like Commerce does for onsite/offsite/manual). The base module should provide a TerminalPaymentGatewayInterface that extends PaymentGatewayInterface, and adds a createPayment() method, just like ManualPaymentGatewayInterface and OnsitePaymentGatewayInterface do. POS can then implement a custom payment form that accepts a payment amount (keypad or otherwise) and a selected payment gateway config entity. Then, if the selected gateway is an instance of ManualPaymentGatewayInterface, it calls $gateway->createPayment(). Otherwise, it checks whether the gateway is an instance of TerminalPaymentGatewayInterface and starts the terminal payment process:

- A “Please wait” animation is shown. Meanwhile, an AJAX request is made to a POS endpoint that calls $gateway->createPayment(), which does the blocking call and returns the result. This will work as long as the Stop button is not hit in the browser (which probably isn’t probable in a POS environment).

As a bonus, the payment form could also support on-site gateways, embedding the add-payment-method payment gateway form, and then using the payment method to create a payment, but this is probably not a highly desired feature, since it would require customers to manually enter their CC number on the screen.

Locations (commerce_pos_location)

In D7 locations are custom entities without bundles, containing a name and an address.
This maps cleanly to the D8 concept of Commerce Stores, so there is no need for this module to exist anymore. Stores by default have an “online” store type. The base commerce_pos module will provide its own store type (named “physical” or something similar). This allows us to limit access (allow users to only view/modify physical stores), limit reference fields, add custom fields more cleanly, etc.

Commerce note: Would be great to move the creation of the “online” store type to the setup wizard, that way POS sites don’t need to have it at all.

Registers (commerce_pos_register)

In D7 registers are custom entities without bundles, containing just of a name. A separate table is provided to track floats & their dates (I am assuming floats are currency amounts?)

In D8 commerce_pos_register will be a config entity provided by the base commerce_pos module. Floats can be tracked with a combination of a service and matching SQL table, similar to promotion usage:

https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/promotio...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/promotio...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/promotio...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/promotio...

Cashiers (commerce_pos_cashier)

In D7 cashiers are custom entities without bundles, containing of a name and secret code.

My initial thought for D8 was to have a commerce_pos_cashier config entity provided by the base commerce_pos module. Shawn suggested making cashiers into regular users (with a “cashier” role), which makes sense from a storage and API perspective. Also makes sense for the order owner to be the cashier.
A few potential problems:
Cashiers are now managed on a far away screen that has many irrelevant options. It would probably make sense to build a custom cashier UI under the POS dashboard which would create/edit/delete users under the hood.
Users need emails, which is a requirement that the old approach didn’t have. Could be autogenerated by the custom UI (username + site domain)
Usernames are global, so there’s room for collision, leading to less user-friendly cashier identifiers. If this becomes a problem, a real name field could be added for display purposes.
It would also probably make sense for the POS UI to have a fast user switching widget (listed usernames, prompted for password on click).

Currency Denominations (commerce_pos_currency_denomination)

In D7 there are currency denomination types and currency denominations, implemented via custom tables (non-entities). A denomination type is USD, for example. Denominations are 1USD, 50USD, pennies, dimes, etc.

In D8 I would go with a commerce_pos_denominations config entity, one per currency, which contains all of the denominations inside. Then the module can provide config/install/commerce_pos_currency_denomination.commerce_pos_currency_denominations.USD.yml.

This might be a good candidate for a non-POS contrib (commerce_currency_denomination)?
If it stays in POS, there should be a discussion about whether it needs to be a submodule VS merging it into the base module.

UPC Scan (commerce_pos_upc_scan)

I would fold this functionality into the main module. The additional UI elements can show up if the administrator has configured UPC (by selecting the variation UPC field). A custom service can perform lookups (commerce_pos.upc, giving you $upc->lookup())

Labels (commerce_pos_label)

This can stay a submodule.
hook_commerce_pos_label_format_info() becomes a YAML plugin, similar to adjustment types.
A module would define label formats in $module.commerce_pos_labels.yml

Now that UPC settings are in the main module, commerce_pos_label_barcode can be merged into commerce_pos_label.

https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/order/co...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/order/sr...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/order/sr...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/order/sr...
https://github.com/drupalcommerce/commerce/blob/8.x-2.x/modules/order/co...

Gratuity and Discount

These two modules used custom line item types and matching UIs.

Commerce 2.x has order items, and they only reference purchased things. Gratuity and discounts are applied as adjustments. POS would define custom adjustment types in commerce_pos.commerce_adjustment_types.yml. Right now adjustment types can’t specify custom UIs (which would be practical for discounts at least), but that is a change that can be made to Commerce.

Messages (commerce_pos_messages)

This module integrates with commerce_message to allow cashiers to add notes to the order.

Commerce Message has been replaced by the Commerce Log module, included with Commerce. It makes sense for the “customer/administrator note” functionality (log template, form) to be added to Commerce itself, and then just reused by Commerce POS.

Reports (commerce_pos_report)

I’d integrate with commerce_reports here. Matt’s new architecture allows for report types, each of which can have custom fields. That makes complete sense for the three different POS kinds of reports. It also allows work on POS to be work on Commerce Reports, instead of duplicating it.

One extra feature to investigate would be a Shopify-like timeline: https://www.shopify.com/pos/features#store-management this could easily be built with the commerce_log Commerce submodule.

Comments

smccabe created an issue. See original summary.

smccabe’s picture

Issue summary: View changes
smccabe’s picture

Version: 7.x-2.x-dev » 8.x-2.x-dev
bojanz’s picture

Issue summary: View changes

Formatting improvements.

bojanz’s picture

Issue summary: View changes

After reviewing several times, I've decided to remove the AdvancedQueue terminal processing method, at least for now.
This was the original text:

AdvancedQueue - If the advancedqueue module is enabled (Note: no D8 port yet) an advancedqueue entry is added on form submit. The background deamon will then take the entry (containing the amount, gateway and order ID), create a payment, load the gateway, pass the payment to $gateway->createPayment(), return an appropriate HTTP code. The payment form can poll advancedqueue to find out whether the queue item was successfuly processed (indicating a created payment) or not (indicating a failure). The benefit of this version is that the POS payment form can be closed or refreshed without interrupting the process.

Some research showed that an alternative to AdvancedQueue could be Direct Queue, which has a D8 port. However, two additional problems showed up:
1) Drupal's queues don't actually have a way to load a queue item by ID, to check its status. That means we'd actually need to create a pending payment and poll that, then delete it on failure, This is a different logic than the Direct method (Please load + Ajax request) which allows us to save the payment only if it was successful.
2) The payment process isn't fundamentally async. The customer is standing right there and waiting. That's why it seems preferable to do the direct method for now.

If I turn out to be horribly wrong here, we can always come back to this feature.

jantoine’s picture

smccabe’s picture

Status: Active » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.