diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index 59020a99245..20ae52674f9 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -177,14 +177,15 @@ class PosSelfOrderController(http.Controller): @http.route('/kiosk/payment//', auth='public', type='json', website=True) def pos_self_order_kiosk_payment(self, pos_config_id, order, payment_method_id, access_token, device_type): + pos_config = self._verify_pos_config(access_token) order_dict = self.process_new_order(order, access_token, None, device_type) if not order_dict.get('id'): raise BadRequest("Something went wrong") # access_token verified in process_new_order - order_sudo = request.env['pos.order'].sudo().browse(order_dict.get('id')) - payment_method_sudo = request.env["pos.payment.method"].sudo().browse(payment_method_id) + order_sudo = pos_config.env['pos.order'].browse(order_dict.get('id')) + payment_method_sudo = pos_config.env["pos.payment.method"].browse(payment_method_id) if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids: raise NotFound("Order or payment method not found") @@ -193,7 +194,7 @@ class PosSelfOrderController(http.Controller): if not status: raise BadRequest("Something went wrong") - return order_sudo._export_for_self_order() + return {'order': order_sudo._export_for_self_order(), 'payment_status': status} def _process_lines(self, lines, pos_config, pos_order_id, take_away=False): appended_uuid = [] diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index 2159de3239d..206c9d3d831 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -333,7 +333,7 @@ class PosConfig(models.Model): def _get_self_ordering_data(self): self.ensure_one() payment_search_params = self.current_session_id._loader_params_pos_payment_method() - payment_methods = self.payment_method_ids.filtered(lambda p: p.use_payment_terminal == 'adyen' or p.is_online_payment).read(payment_search_params['search_params']['fields']) + payment_methods = self.payment_method_ids.filtered(lambda p: p.use_payment_terminal in ['adyen', 'stripe'] or p.is_online_payment).read(payment_search_params['search_params']['fields']) default_language = self.self_ordering_default_language_id.read(["code", "name", "iso_code", "flag_image_url"]) return { diff --git a/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.js b/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.js index dbac75f586f..53dbfec326b 100644 --- a/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.js +++ b/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.js @@ -55,11 +55,12 @@ export class PaymentPage extends Component { async startPayment() { this.selfOrder.paymentError = false; try { - const order = await this.rpc(`/kiosk/payment/${this.selfOrder.pos_config_id}/kiosk`, { + const result = await this.rpc(`/kiosk/payment/${this.selfOrder.pos_config_id}/kiosk`, { order: this.selfOrder.currentOrder, access_token: this.selfOrder.access_token, payment_method_id: this.state.paymentMethodId, }); + const order = result.order; this.selfOrder.updateOrderFromServer(order); } catch (error) { this.selfOrder.handleErrorNotification(error); diff --git a/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.xml b/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.xml index 2527e56510a..c1e135ad4d3 100644 --- a/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.xml +++ b/addons/pos_self_order/static/src/app/pages/payment_page/payment_page.xml @@ -23,7 +23,7 @@
- +
diff --git a/addons/pos_self_order/static/src/app/self_order_service.js b/addons/pos_self_order/static/src/app/self_order_service.js index 30161e6e5b9..db33bb46c77 100644 --- a/addons/pos_self_order/static/src/app/self_order_service.js +++ b/addons/pos_self_order/static/src/app/self_order_service.js @@ -154,6 +154,16 @@ export class SelfOrder extends Reactive { initKioskData() { this.ordering = true; + this.idleTimout = false; + + window.addEventListener("click", (event) => { + this.idleTimout && clearTimeout(this.idleTimout); + this.idleTimout = setTimeout(() => { + if (this.router.activeSlot !== "payment") { + this.router.navigate("default"); + } + }, 5 * 1000 * 60); + }); } async initMobileData() { diff --git a/addons/pos_self_order_adyen/models/pos_payment_method.py b/addons/pos_self_order_adyen/models/pos_payment_method.py index f7d68a33625..2058ae8d9aa 100644 --- a/addons/pos_self_order_adyen/models/pos_payment_method.py +++ b/addons/pos_self_order_adyen/models/pos_payment_method.py @@ -8,7 +8,7 @@ class PosPaymentMethod(models.Model): def payment_request_from_kiosk(self, order): if self.use_payment_terminal != 'adyen': - super().payment_request_from_kiosk(order) + return super().payment_request_from_kiosk(order) else: pos_config = order.session_id.config_id random_number = random.randrange(10**9, 10**10 - 1) diff --git a/addons/pos_self_order_stripe/__init__.py b/addons/pos_self_order_stripe/__init__.py new file mode 100644 index 00000000000..c3d410ea160 --- /dev/null +++ b/addons/pos_self_order_stripe/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/addons/pos_self_order_stripe/__manifest__.py b/addons/pos_self_order_stripe/__manifest__.py new file mode 100644 index 00000000000..7cca54ad885 --- /dev/null +++ b/addons/pos_self_order_stripe/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +{ + "name": "POS Self Order Stripe", + "summary": "Addon for the Self Order App that allows customers to pay by Stripe.", + "category": "Sales/Point Of Sale", + "depends": ["pos_stripe", "pos_self_order"], + "auto_install": True, + 'data': [ + 'views/assets_stripe.xml', + ], + 'assets': { + 'pos_self_order.assets': [ + 'pos_self_order_stripe/static/**/*', + ], + }, + "license": "LGPL-3", +} diff --git a/addons/pos_self_order_stripe/controllers/__init__.py b/addons/pos_self_order_stripe/controllers/__init__.py new file mode 100644 index 00000000000..4d7317075a3 --- /dev/null +++ b/addons/pos_self_order_stripe/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import orders diff --git a/addons/pos_self_order_stripe/controllers/orders.py b/addons/pos_self_order_stripe/controllers/orders.py new file mode 100644 index 00000000000..e7c3d3301d2 --- /dev/null +++ b/addons/pos_self_order_stripe/controllers/orders.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from odoo import http, fields +from odoo.http import request +from odoo.tools import float_is_zero +from odoo.addons.pos_self_order.controllers.orders import PosSelfOrderController +from werkzeug.exceptions import Unauthorized + +class PosSelfOrderControllerStripe(PosSelfOrderController): + @http.route("/pos-self-order/stripe-connection-token/", auth="public", type="json", website=True) + def get_stripe_creditentials(self, access_token, payment_method_id): + # stripe_connection_token + pos_config, _ = self._verify_authorization(access_token, "", False) + payment_method = pos_config.payment_method_ids.filtered(lambda p: p.id == payment_method_id) + return payment_method.stripe_connection_token() + + @http.route("/pos-self-order/stripe-capture-payment/", auth="public", type="json", website=True) + def stripe_capture_payment(self, access_token, order_access_token, payment_intent_id, payment_method_id): + pos_config, _ = self._verify_authorization(access_token, "", False) + stripe_confirmation = pos_config.env['pos.payment.method'].stripe_capture_payment(payment_intent_id) + order = pos_config.env['pos.order'].search([('access_token', '=', order_access_token), ('config_id', '=', pos_config.id)]) + + if not order: + raise Unauthorized() + + payment_method = pos_config.payment_method_ids.filtered(lambda p: p.id == payment_method_id) + stripe_order_amount = payment_method._stripe_calculate_amount(order.amount_total) + + if float_is_zero(stripe_order_amount - stripe_confirmation['amount'], precision_rounding=pos_config.currency_id.rounding) and stripe_confirmation['status'] == 'succeeded': + transaction_id = stripe_confirmation['id'] + payment_result = stripe_confirmation['status'] + + order.add_payment({ + 'amount': order.amount_total, + 'payment_date': fields.Datetime.now(), + 'payment_method_id': payment_method.id, + 'card_type': False, + 'cardholder_name': '', + 'transaction_id': transaction_id, + 'payment_status': payment_result, + 'ticket': '', + 'pos_order_id': order.id + }) + + order.action_pos_order_paid() + + if order.config_id.self_ordering_mode == 'kiosk': + request.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', { + 'payment_result': 'Success', + 'order': order._export_for_self_order(), + }) + else: + request.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', { + 'payment_result': 'fail', + 'order': order._export_for_self_order(), + }) diff --git a/addons/pos_self_order_stripe/models/__init__.py b/addons/pos_self_order_stripe/models/__init__.py new file mode 100644 index 00000000000..7f48bb56cba --- /dev/null +++ b/addons/pos_self_order_stripe/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import pos_payment_method diff --git a/addons/pos_self_order_stripe/models/pos_payment_method.py b/addons/pos_self_order_stripe/models/pos_payment_method.py new file mode 100644 index 00000000000..47364852e61 --- /dev/null +++ b/addons/pos_self_order_stripe/models/pos_payment_method.py @@ -0,0 +1,11 @@ +from odoo import models + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + def payment_request_from_kiosk(self, order): + if self.use_payment_terminal != 'stripe': + return super().payment_request_from_kiosk(order) + else: + return self.stripe_payment_intent(order.amount_total) diff --git a/addons/pos_self_order_stripe/static/src/app/pages/payment_page/payment_page.js b/addons/pos_self_order_stripe/static/src/app/pages/payment_page/payment_page.js new file mode 100644 index 00000000000..e51e5fdd688 --- /dev/null +++ b/addons/pos_self_order_stripe/static/src/app/pages/payment_page/payment_page.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { PaymentPage } from "@pos_self_order/app/pages/payment_page/payment_page"; + +patch(PaymentPage.prototype, { + async startPayment() { + this.selfOrder.paymentError = false; + const paymentMethod = this.selfOrder.pos_payment_methods.find( + (p) => p.id === this.state.paymentMethodId + ); + + if (paymentMethod.use_payment_terminal === "stripe") { + await this.selfOrder.stripe.startPayment(this.selfOrder.currentOrder); + } else { + await super.startPayment(...arguments); + } + }, +}); diff --git a/addons/pos_self_order_stripe/static/src/app/self_order_service.js b/addons/pos_self_order_stripe/static/src/app/self_order_service.js new file mode 100644 index 00000000000..e51c1dac271 --- /dev/null +++ b/addons/pos_self_order_stripe/static/src/app/self_order_service.js @@ -0,0 +1,49 @@ +/** @odoo-module */ +import { patch } from "@web/core/utils/patch"; +import { SelfOrder } from "@pos_self_order/app/self_order_service"; +import { Stripe, StripeError } from "@pos_self_order_stripe/app/stripe"; + +patch(SelfOrder.prototype, { + async setup() { + await super.setup(...arguments); + this.stripeState = "not_connected"; + + const stripePaymentMethod = this.pos_payment_methods.find( + (p) => p.use_payment_terminal === "stripe" + ); + + if (stripePaymentMethod) { + this.stripe = new Stripe( + this.env, + stripePaymentMethod, + this.access_token, + this.pos_config_id, + this.handleStripeError.bind(this), + this.handleReaderConnection.bind(this) + ); + } + }, + handleReaderConnection(state) { + this.stripeState = state.status; + }, + handleStripeError(error) { + this.paymentError = true; + this.handleErrorNotification(error); + }, + handleErrorNotification(error) { + let message = ""; + + if (error.code) { + message = `Error: ${error.code}`; + } else if (error instanceof StripeError) { + message = `Stripe: ${error.message}`; + } else { + super.handleErrorNotification(...arguments); + return; + } + + this.notification.add(message, { + type: "danger", + }); + }, +}); diff --git a/addons/pos_self_order_stripe/static/src/app/stripe.js b/addons/pos_self_order_stripe/static/src/app/stripe.js new file mode 100644 index 00000000000..2e90f2600aa --- /dev/null +++ b/addons/pos_self_order_stripe/static/src/app/stripe.js @@ -0,0 +1,139 @@ +/** @odoo-module **/ +/* global StripeTerminal */ + +export class StripeError extends Error {} + +export class Stripe { + constructor(...args) { + this.setup(...args); + } + + setup( + env, + stripePaymentMethod, + access_token, + pos_config_id, + errorCallback, + handleReaderConnection + ) { + this.env = env; + this.terminal = null; + this.access_token = access_token; + this.stripePaymentMethod = stripePaymentMethod; + this.pos_config_id = pos_config_id; + this.errorCallback = errorCallback; + this.handleReaderConnection = handleReaderConnection; + + this.createTerminal(); + } + + get connectionStatus() { + return this.terminal.getConnectionStatus(); + } + + createTerminal() { + this.terminal = StripeTerminal.create({ + onFetchConnectionToken: this.getBackendConnectionToken.bind(this), + onConnectionStatusChange: this.handleReaderConnection.bind(this), + onUnexpectedReaderDisconnect: () => { + this.handleReaderConnection({ + status: "not_connected", + }); + }, + }); + } + + async startPayment(order) { + try { + const result = await this.env.services.rpc( + `/kiosk/payment/${this.pos_config_id}/kiosk`, + { + order: order, + access_token: this.access_token, + payment_method_id: this.stripePaymentMethod.id, + } + ); + const paymentStatus = result.payment_status; + const savedOrder = result.order; + await this.connectReader(); + const clientSecret = paymentStatus.client_secret; + const paymentMethod = await this.collectPaymentMethod(clientSecret); + const processPayment = await this.processPayment(paymentMethod.paymentIntent); + await this.capturePayment(processPayment.paymentIntent.id, savedOrder); + } catch (error) { + this.errorCallback(error); + } + } + + async processPayment(paymentIntent) { + const result = await this.terminal.processPayment(paymentIntent); + + if (result.error) { + throw new StripeError(result.error.code); + } + + return result; + } + + async getBackendConnectionToken() { + const data = await this.env.services.rpc("/pos-self-order/stripe-connection-token", { + access_token: this.access_token, + payment_method_id: this.stripePaymentMethod.id, + }); + + return data.secret; + } + + async capturePayment(paymentIntentId, order) { + return await this.env.services.rpc("/pos-self-order/stripe-capture-payment", { + access_token: this.access_token, + order_access_token: order.access_token, + payment_intent_id: paymentIntentId, + payment_method_id: this.stripePaymentMethod.id, + }); + } + + async discoverReaders() { + const result = await this.terminal.discoverReaders({ + allowCustomerCancel: true, + }); + + if (result.error) { + throw new StripeError(result.error.code); + } + + return result; + } + + async connectReader() { + if (this.connectionStatus !== "not_connected") { + return; + } + + const discoverReaders = await this.discoverReaders(); + const discoveredReaders = discoverReaders.discoveredReaders; + const findLinkedReader = discoveredReaders.find( + (reader) => reader.serial_number == this.stripePaymentMethod.stripe_serial_number + ); + + const result = await this.terminal.connectReader(findLinkedReader, { + fail_if_in_use: true, + }); + + if (result.error) { + throw new StripeError(result.error.code); + } + + return result; + } + + async collectPaymentMethod(clientSecret) { + const result = await this.terminal.collectPaymentMethod(clientSecret); + + if (result.error) { + throw new StripeError(result.error.code); + } + + return result; + } +} diff --git a/addons/pos_self_order_stripe/views/assets_stripe.xml b/addons/pos_self_order_stripe/views/assets_stripe.xml new file mode 100644 index 00000000000..8ca735a9e98 --- /dev/null +++ b/addons/pos_self_order_stripe/views/assets_stripe.xml @@ -0,0 +1,10 @@ + + + +