refactor: extract cart storage to dedicated API controller with dynamic frontend URLs
All checks were successful
Ruby on Rails Test / rails-test (push) Successful in 1m7s
All checks were successful
Ruby on Rails Test / rails-test (push) Successful in 1m7s
- Added dedicated CartsController for session-based cart storage - Refactored routes to use POST /api/v1/carts/store - Updated ticket selection JS to use dynamic data attributes for URLs - Fixed CSRF protection in API and checkout payment increment - Made checkout button URLs dynamic via data attributes - Updated tests for new cart storage endpoint - Removed obsolete store_cart from EventsController
This commit is contained in:
25
app/controllers/api/v1/carts_controller.rb
Normal file
25
app/controllers/api/v1/carts_controller.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
class CartsController < ApiController
|
||||||
|
# Skip API key authentication for store_cart action (used by frontend forms)
|
||||||
|
skip_before_action :authenticate_api_key, only: [ :store ]
|
||||||
|
|
||||||
|
def store
|
||||||
|
event_id = params[:event_id]
|
||||||
|
@event = Event.find(event_id)
|
||||||
|
|
||||||
|
cart_data = params[:cart] || {}
|
||||||
|
session[:pending_cart] = cart_data
|
||||||
|
session[:event_id] = @event.id
|
||||||
|
|
||||||
|
render json: { status: "success", message: "Cart stored successfully" }
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render json: { status: "error", message: "Event not found" }, status: :not_found
|
||||||
|
rescue => e
|
||||||
|
error_message = e.message.present? ? e.message : "Unknown error"
|
||||||
|
Rails.logger.error "Error storing cart: #{error_message}"
|
||||||
|
render json: { status: "error", message: "Failed to store cart" }, status: 500
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,9 +4,6 @@
|
|||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class OrdersController < ApiController
|
class OrdersController < ApiController
|
||||||
# Skip API key authentication for store_cart action (used by frontend forms)
|
|
||||||
skip_before_action :authenticate_api_key, only: [ :store_cart ]
|
|
||||||
|
|
||||||
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ]
|
||||||
before_action :set_event, only: [ :new, :create ]
|
before_action :set_event, only: [ :new, :create ]
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Provides authentication and common functionality for API controllers
|
# Provides authentication and common functionality for API controllers
|
||||||
class ApiController < ApplicationController
|
class ApiController < ApplicationController
|
||||||
# Disable CSRF protection for API requests (token-based authentication instead)
|
# Disable CSRF protection for API requests (token-based authentication instead)
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery prepend: true
|
||||||
|
|
||||||
# Authenticate all API requests using API key
|
# Authenticate all API requests using API key
|
||||||
# Must be called before any API action
|
# Must be called before any API action
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default class extends Controller {
|
|||||||
"checkoutButton",
|
"checkoutButton",
|
||||||
"form",
|
"form",
|
||||||
];
|
];
|
||||||
static values = { eventSlug: String, eventId: String };
|
static values = { eventSlug: String, eventId: String, orderNewUrl: String, storeCartUrl: String };
|
||||||
|
|
||||||
// Initialize the controller and update the cart summary
|
// Initialize the controller and update the cart summary
|
||||||
connect() {
|
connect() {
|
||||||
@@ -117,9 +117,9 @@ export default class extends Controller {
|
|||||||
// Store cart data in session
|
// Store cart data in session
|
||||||
await this.storeCartInSession(cartData);
|
await this.storeCartInSession(cartData);
|
||||||
|
|
||||||
// Redirect to event-scoped orders/new page
|
// Redirect to event-scoped orders/new page
|
||||||
const OrderNewUrl = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`;
|
const orderNewUrl = this.orderNewUrlValue;
|
||||||
window.location.href = OrderNewUrl;
|
window.location.href = orderNewUrl;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error storing cart:", error);
|
console.error("Error storing cart:", error);
|
||||||
alert("Une erreur est survenue. Veuillez réessayer.");
|
alert("Une erreur est survenue. Veuillez réessayer.");
|
||||||
@@ -145,7 +145,7 @@ export default class extends Controller {
|
|||||||
|
|
||||||
// Store cart data in session via AJAX
|
// Store cart data in session via AJAX
|
||||||
async storeCartInSession(cartData) {
|
async storeCartInSession(cartData) {
|
||||||
const storeCartUrl = `/api/v1/events/${this.eventIdValue}/store_cart`;
|
const storeCartUrl = this.storeCartUrlValue;
|
||||||
|
|
||||||
const response = await fetch(storeCartUrl, {
|
const response = await fetch(storeCartUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -155,7 +155,7 @@ export default class extends Controller {
|
|||||||
.querySelector('meta[name="csrf-token"]')
|
.querySelector('meta[name="csrf-token"]')
|
||||||
.getAttribute("content"),
|
.getAttribute("content"),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ cart: cartData }),
|
body: JSON.stringify({ cart: cartData, event_id: this.eventIdValue }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -135,8 +135,12 @@
|
|||||||
controller: "ticket-selection",
|
controller: "ticket-selection",
|
||||||
ticket_selection_target: "form",
|
ticket_selection_target: "form",
|
||||||
ticket_selection_event_slug_value: @event.slug,
|
ticket_selection_event_slug_value: @event.slug,
|
||||||
ticket_selection_event_id_value: @event.id
|
ticket_selection_event_id_value: @event.id,
|
||||||
} do |form| %>
|
ticket_selection_order_new_url_value: event_order_new_path(@event.slug, @event.id),
|
||||||
|
ticket_selection_store_cart_url_value: api_v1_store_cart_path,
|
||||||
|
ticket_selection_order_new_url_value: event_order_new_path(@event.slug, @event.id),
|
||||||
|
ticket_selection_store_cart_url_value: api_v1_store_cart_path
|
||||||
|
} do |form| %>
|
||||||
|
|
||||||
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
<div class="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl border border-purple-100 p-6 shadow-sm">
|
||||||
<div class="flex justify-center sm:justify-start mb-6">
|
<div class="flex justify-center sm:justify-start mb-6">
|
||||||
|
|||||||
@@ -139,10 +139,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="checkout-button"
|
id="checkout-button"
|
||||||
class="w-full btn btn-primary py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl"
|
data-order-id="<%= @order.id %>"
|
||||||
>
|
data-increment-url="/api/v1/orders/<%= @order.id %>/increment_payment_attempt"
|
||||||
|
data-session-id="<%= @checkout_session.id if @checkout_session.present? %>"
|
||||||
|
class="w-full btn btn-primary py-4 px-6 rounded-xl transition-all duration-200 transform hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
<i data-lucide="credit-card" class="w-5 h-5 mr-2"></i>
|
||||||
Payer <%= @order.total_amount_euros %>€
|
Payer <%= @order.total_amount_euros %>€
|
||||||
@@ -199,13 +202,16 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Increment payment attempt counter
|
// Increment payment attempt counter
|
||||||
console.log('Incrementing payment attempt for order:', '<%= @order.id %>');
|
const orderId = checkoutButton.dataset.orderId;
|
||||||
const response = await fetch('/api/v1/orders/<%= @order.id %>/increment_payment_attempt', {
|
const incrementUrl = checkoutButton.dataset.incrementUrl;
|
||||||
method: 'PATCH',
|
console.log('Incrementing payment attempt for order:', orderId);
|
||||||
headers: {
|
const response = await fetch(incrementUrl, {
|
||||||
'Content-Type': 'application/json'
|
method: 'PATCH',
|
||||||
}
|
headers: {
|
||||||
});
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('[name=csrf-token]').content
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Payment attempt increment failed:', response.status, response.statusText);
|
console.error('Payment attempt increment failed:', response.status, response.statusText);
|
||||||
@@ -226,10 +232,11 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Redirect to Stripe
|
// Redirect to Stripe
|
||||||
console.log('Redirecting to Stripe with session ID:', '<%= @checkout_session&.id %>');
|
const sessionId = checkoutButton.dataset.sessionId;
|
||||||
const stripeResult = await stripe.redirectToCheckout({
|
console.log('Redirecting to Stripe with session ID:', sessionId);
|
||||||
sessionId: '<%= @checkout_session.id %>'
|
const stripeResult = await stripe.redirectToCheckout({
|
||||||
});
|
sessionId: sessionId
|
||||||
|
});
|
||||||
|
|
||||||
if (stripeResult.error) {
|
if (stripeResult.error) {
|
||||||
throw new Error(stripeResult.error.message);
|
throw new Error(stripeResult.error.message);
|
||||||
|
|||||||
@@ -96,11 +96,8 @@ Rails.application.routes.draw do
|
|||||||
namespace :api do
|
namespace :api do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
# RESTful routes for event management
|
# RESTful routes for event management
|
||||||
resources :events, only: [ :index, :show, :create, :update, :destroy ] do
|
resources :events, only: [ :index, :show, :create, :update, :destroy ]
|
||||||
member do
|
post "carts/store", to: "carts#store", as: "store_cart"
|
||||||
post :store_cart
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# RESTful routes for order management
|
# RESTful routes for order management
|
||||||
resources :orders, only: [] do
|
resources :orders, only: [] do
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Api::V1::EventsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "should store cart" do
|
test "should store cart" do
|
||||||
post store_cart_api_v1_event_url(@event), params: { cart: { ticket_type_id: 1, quantity: 2 } }, as: :json
|
post api_v1_store_cart_path, params: { cart: { ticket_type_id: 1, quantity: 2 }, event_id: @event.id }, as: :json, headers: headers_api_key
|
||||||
assert_response :success
|
assert_response :success
|
||||||
assert_equal @event.id, session[:event_id]
|
assert_equal @event.id, session[:event_id]
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user