qr pay updates

- subscribe to action cable from cloud and listen
- callback from cloud to local and show success payment in real time
- payment service and process payment after callback
This commit is contained in:
Dev Team
2025-05-26 11:29:33 +06:30
parent a43f8ed4f6
commit 1768345299
5 changed files with 208 additions and 21 deletions

View File

@@ -1,5 +1,8 @@
class Foodcourt::QrpayController < BaseFoodcourtController class Foodcourt::QrpayController < BaseFoodcourtController
require 'rqrcode' require 'rqrcode'
skip_before_action :authenticate, only: [:create]
skip_before_action :verify_authenticity_token, only: [:create]
def init def init
@cash_exist = PaymentMethodSetting.cash_exist @cash_exist = PaymentMethodSetting.cash_exist
@@ -217,6 +220,28 @@ class Foodcourt::QrpayController < BaseFoodcourtController
end end
end end
def create
sale_id = params[:sale_id]
unless current_login_employee
render json: { status: false, message: "User not authenticated or employee context missing." }, status: :unauthorized
return
end
qrpayment_service = QrPaymentService.new(sale_id, current_login_employee)
result = qrpayment_service.process
if result[:status]
render json: result, status: :ok
else
status_code = result[:message].include?("not found") ? :not_found : :unprocessable_entity
render json: result, status: status_code
end
end
def test_pay
end
def cancel def cancel
# cancel orders and related # cancel orders and related
sale_id = params[:sale_id] sale_id = params[:sale_id]

View File

@@ -0,0 +1,61 @@
class QrPaymentService
attr_reader :sale_id, :current_login_employee, :sale, :booking, :processed_sale_payment
def initialize(sale_id, current_login_employee)
@sale_id = sale_id
@current_login_employee = current_login_employee
end
def process
return { status: false, message: "Sale ID is required." } unless sale_id.present?
SalePayment.transaction do
find_sale_and_booking
return { status: false, message: "There is no sale for '#{sale_id}'!" } unless sale
return { status: false, message: "Already paid for '#{sale_id}'!" } unless sale.sale_status == "new"
status, payment_attempt = process_the_payment
unless status
error_message = payment_attempt&.errors&.full_messages&.join(', ') || "Payment processing failed."
return { status: false, message: error_message }
end
process_orders_and_broadcast
return { status: true, message: "Payment successful." }
end
rescue ActiveRecord::RecordNotFound
{ status: false, message: "Sale not found for ID '#{sale_id}'." }
rescue StandardError => e
Rails.logger.error "QrPaymentService Error: #{e.message}\n#{e.backtrace.join("\n")}"
{ status: false, message: "An unexpected error occurred: #{e.message}" }
end
private
def find_sale_and_booking
@sale = Sale.find_by_sale_id(sale_id) # Consider find_by_sale_id! if you want RecordNotFound earlier
@booking = @sale&.booking
end
def process_the_payment
sale_payment_creator = SalePayment.new
status, payment = sale_payment_creator.process_payment(
sale,
current_login_employee,
sale.grand_total,
"MMQR"
)
[status, payment]
end
def process_orders_and_broadcast
booking.orders.each do |order|
oqs = OrderQueueStation.new
oqs.pay_process_order_queue(order.order_id, booking.dining_facility_id)
assign_order = AssignedOrderItem.assigned_order_item_by_job(order.order_id)
ActionCable.server.broadcast "order_queue_station_channel", order: assign_order
end
end
end

View File

@@ -247,29 +247,87 @@
<script defer type="text/javascript"> <script defer type="text/javascript">
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const paymentWaiting = document.querySelector('.payment-waiting'); const paymentWaiting = document.querySelector('.payment-waiting');
let amountToReceive = <%= number_with_precision(@sale_data.grand_total, precision: precision.to_i) %> let amountToReceive = <%= number_with_precision(@sale_data.grand_total, precision: precision.to_i) %>;
const ws = new WebSocket("wss://juicecorner-0mo.sx-fc.app/cable");
function checkPaymentStatus() { ws.onopen = () => {
// fetch('') console.log("Nagato channel connected");
// .then(response => response.json())
// .then(data => { ws.send(JSON.stringify({
// if (data.paid) { command: "subscribe",
// paymentWaiting.innerHTML = ` identifier: JSON.stringify({
// <div class="payment-success text-center"> channel: "TestChannel"
// <i class="material-icons" style="font-size: 50px; color: #4CAF50;">check_circle</i> })
// <h3 class="m-t-20" style="color: #4CAF50;">Payment Successful!</h3> }));
// <p>Amount Received: ${amountToReceive}</p>
// </div>
// `;
// } else {
// setTimeout(checkPaymentStatus, 3000);
// }
// });
} }
// Start checking payment status ws.onmessage = (e) => {
checkPaymentStatus(); const msg = JSON.parse(e.data);
console.log("Received:", msg)
if(msg.type === 'confirm_subscription') {
console.log("This world shall know pain");
}
if(msg?.message?.status === "PAY_SUCCESS") {
paymentWaiting.innerHTML = `
<div class="payment-success text-center">
<i class="material-icons" style="font-size: 50px; color: #4CAF50;">check_circle</i>
<h3 class="m-t-20" style="color: #4CAF50;">Payment Successful!</h3>
<p>Amount Received: ${amountToReceive}</p>
</div>
`;
ws.close();
// fetch('/foodcourt/qrpay/process_payment', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
// },
// body: JSON.stringify({ sale_id: "SAL-0qg000000671" })
// })
fetch('/foodcourt/qrpay/process_payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ sale_id: "<%= @sale_data.sale_id %>" })
}).then((res) => res.json())
.then((data) => {
if(data.status) {
customer_display_view(null, "reload");
setTimeout(() => {
window.location.href = "/";
}, 3000)
}else {
console.log("error:", data);
}
}).catch((e) => console.log(e))
;
}
}
ws.onerror = (e) => {
console.log(error)
}
function customer_display_view(data, status) {
let url = '/foodcourt/customer_view';
$.ajax({
type: "POST",
url: url,
data: { data: data, status: status },
dataType: "json",
success:function(result){
}
});
}
// cancel order
document.querySelector('#cancel-btn').addEventListener('click', function(e) { document.querySelector('#cancel-btn').addEventListener('click', function(e) {
const data = { const data = {
sale_id: "<%= @sale_data.sale_id %>", sale_id: "<%= @sale_data.sale_id %>",

View File

@@ -0,0 +1,43 @@
<div id="status">Connecting...</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const ws = new WebSocket("wss://juicecorner-0mo.sx-fc.app/cable")
ws.onopen = () => {
console.log("WS Connected");
ws.send(JSON.stringify({
command: "subscribe",
identifier: JSON.stringify({
channel: "TestChannel"
})
}))
document.getElementById('status').textContent = 'Waiting for payment...'
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
console.log("Received:", msg)
// Handle subscription confirmation
if(msg.type === "confirm_subscription") {
console.log("Successfully subscribed!")
return
}
// Handle payment success
if(msg?.message?.status === "PAY_SUCCESS") {
document.getElementById('status').innerHTML = `
<h3 style="color: green;">Payment Received!</h3>
<p>Amount: ${msg.message.amount} ${msg.message.currency}</p>
`
ws.close()
}
}
ws.onerror = (e) => {
console.error("WS Error:", e)
document.getElementById('status').textContent = 'Connection failed'
}
})
</script>

View File

@@ -677,8 +677,6 @@ scope "(:locale)", locale: /en|mm/ do
get 'sale/:sale_id' => 'sales#show' get 'sale/:sale_id' => 'sales#show'
get 'order/:order_id' => "orders#show" get 'order/:order_id' => "orders#show"
get 'qrpay/payment_loading' => 'qrpay#payment_loading'
# Other Charges # Other Charges
get "/:sale_id/:type/other_charges" => "other_charges#index" get "/:sale_id/:type/other_charges" => "other_charges#index"
post "/:sale_id/other_charges" => "other_charges#create" post "/:sale_id/other_charges" => "other_charges#create"
@@ -750,6 +748,8 @@ scope "(:locale)", locale: /en|mm/ do
get '/:sale_id/qrpay/init' => 'qrpay#init' get '/:sale_id/qrpay/init' => 'qrpay#init'
post 'qrpay/cancel' => 'qrpay#cancel' post 'qrpay/cancel' => 'qrpay#cancel'
get 'qrpay/test-payment' => 'qrpay#test_payment'
post 'qrpay/process_payment' => 'qrpay#create'
end end
end end
end end