Add api endpoing to create customer transactions

Plus specs and documentation
This commit is contained in:
Gaetan Craig-Riou
2026-01-20 12:13:53 +11:00
parent 6915836a14
commit be7be9bbc6
8 changed files with 329 additions and 6 deletions

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionController < Api::V1::BaseController
def create
authorize! :create, CustomerAccountTransaction
# We only allow using the api customer credit payment method
default_params = { currency: CurrentConfig.get(:currency), payment_method_id: }
transaction = CustomerAccountTransaction.new(
default_params.merge(customer_account_transaction_params)
)
if transaction.save
render json: Api::V1::CustomerAccountTransactionSerializer.new(transaction),
status: :created
else
invalid_resource! transaction
end
end
private
def customer_account_transaction_params
params.require(:customer_account_transaction).permit(:customer_id, :amount, :description)
end
def payment_method_id
Spree::PaymentMethod.internal.find_by(
name: Rails.application.config.api_payment_method[:name]
)&.id
end
end
end
end

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
class CustomerAccountTransactionSchema < JsonApiSchema
def self.object_name
"customer_account_transaction"
end
def self.attributes
{
id: { type: :integer, example: 1 },
customer_id: { type: :integer, example: 10 },
amount: { type: :decimal, example: 10.50 },
currency: { type: :string, example: "AUD" },
payment_method_id: { type: :integer, example: 100 },
description: { type: :string, nullable: true, example: "Payment processed by POS" },
balance: { type: :decimal, example: 10.50 },
}
end
def self.required_attributes
[:customer_id, :amount]
end
def self.writable_attributes
attributes.except(:id, :balance, :payment_method_id, :currency)
end
def self.relationships
[:customer, :payment_method]
end
end

View File

@@ -61,6 +61,7 @@ module Spree
add_manage_line_items_abilities user
end
add_relationship_management_abilities user if can_manage_relationships? user
add_customer_payment_abilities user if can_manage_enterprises? user
end
# New users have no enterprises.
@@ -457,5 +458,9 @@ module Spree
user.enterprises.include?(enterprise_relationship.child)
end
end
def add_customer_payment_abilities(_user)
can [:create], CustomerAccountTransaction
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionSerializer < Api::V1::BaseSerializer
attributes :id, :customer_id, :payment_method_id, :amount, :currency, :description, :balance
end
end
end

View File

@@ -92,6 +92,8 @@ Openfoodnetwork::Application.routes.draw do
resources :enterprises do
resources :customers, only: :index
end
resources :customer_account_transaction, only: [:create]
end
match '*path', to: redirect(path: "/api/v0/%{path}"), via: :all,

View File

@@ -0,0 +1,112 @@
# frozen_string_literal: true
require "swagger_helper"
RSpec.describe "CustomerAccountTransactions", swagger_doc: "v1.yaml", feature: :api_v1 do
let!(:enterprise) { create(:enterprise) }
let(:payment_method) {
create(
:payment_method,
name: CustomerAccountTransaction::DEFAULT_PAYMENT_METHOD_NAME,
distributors: [enterprise]
)
}
let(:customer) { create(:customer) }
before do
login_as enterprise.owner
end
path "/api/v1/customer_account_transaction" do
post "Create customer transaction" do
tags "Customer account transaction"
consumes "application/json"
produces "application/json"
parameter name: :customer_account_transaction, in: :body, schema: {
type: :object,
properties: CustomerAccountTransactionSchema.writable_attributes,
required: CustomerAccountTransactionSchema.required_attributes
}
response "201", "Customer transaction created" do
let(:customer_account_transaction) do
{
customer_id: customer.id.to_s,
amount: "10.25",
description: "Payment processed by POS"
}
end
schema '$ref': "#/components/schemas/customer_account_transaction"
run_test! do
expect(json_response[:data][:attributes]).to include(
customer_id: customer.id,
payment_method_id: payment_method.id,
amount: "10.25",
currency: "AUD",
description: "Payment processed by POS",
balance: "10.25",
)
transaction = CustomerAccountTransaction.find(json_response[:data][:attributes][:id])
expect(transaction).not_to be_nil
end
end
response "422", "Unpermitted parameter" do
let(:customer_account_transaction) do
{
id: 101,
customer_id: customer.id.to_s,
amount: "10.25",
}
end
schema '$ref': "#/components/schemas/error_response"
run_test! do
expect(json_response[:errors][0][:detail]).to eq(
"Parameters not allowed in this request: id"
)
end
end
response "422", "Unprocessable entity" do
let(:customer_account_transaction) { {} }
schema '$ref': "#/components/schemas/error_response"
run_test! do
expect(json_response[:errors][0][:detail]).to eq(
"A required parameter is missing or empty: customer_account_transaction"
)
expect(json_response[:meta]).to eq nil
end
end
response "422", "Invalid resource" do
let(:customer_account_transaction) { { amount: "10.25" } }
schema '$ref': "#/components/schemas/error_response"
run_test! do
expect(json_response[:errors][0][:detail]).to eq(
"Invalid resource. Please fix errors and try again."
)
expect(json_response[:meta][:validation_errors]).to eq ["Customer must exist"]
end
end
response "401", "Unauthorized" do
before { login_as nil }
let(:customer_account_transaction) do
{
customer_id: customer.id.to_s,
amount: "10.25",
}
end
run_test!
end
end
end
end

View File

@@ -27,7 +27,9 @@ RSpec.configure do |config|
error_response: ErrorsSchema.schema,
# only customer#show is with extra_fields: {name: :balance, required: true}
customer: CustomerSchema.schema(require_all: true),
customers_collection: CustomerSchema.collection(require_all: true, extra_fields: :balance)
customers_collection: CustomerSchema.collection(require_all: true,
extra_fields: :balance),
customer_account_transaction: CustomerAccountTransactionSchema.schema(require_all: true)
},
securitySchemes: {
api_key_header: {

View File

@@ -77,7 +77,7 @@ components:
billing_address:
type: object
nullable: true
example:
example:
shipping_address:
type: object
nullable: true
@@ -190,7 +190,7 @@ components:
billing_address:
type: object
nullable: true
example:
example:
shipping_address:
type: object
nullable: true
@@ -283,6 +283,92 @@ components:
- data
- meta
- links
customer_account_transaction:
type: object
properties:
data:
type: object
properties:
id:
type: string
example: '1'
type:
type: string
example: customer_account_transaction
attributes:
type: object
properties:
id:
type: integer
example: 1
customer_id:
type: integer
example: 10
amount:
type: decimal
example: 10.5
currency:
type: string
example: AUD
payment_method_id:
type: integer
example: 100
description:
type: string
nullable: true
example: Payment processed by POS
balance:
type: decimal
example: 10.5
required:
- id
- customer_id
- amount
- currency
- payment_method_id
- description
- balance
relationships:
type: object
properties:
customer:
type: object
properties:
data:
type: object
properties:
id:
type: string
type:
type: string
example: customer
links:
type: object
properties:
related:
type: string
payment_method:
type: object
properties:
data:
type: object
properties:
id:
type: string
type:
type: string
example: payment_method
links:
type: object
properties:
related:
type: string
meta:
type: object
links:
type: object
required:
- data
securitySchemes:
api_key_header:
type: apiKey
@@ -300,6 +386,46 @@ components:
in: cookie
description: Authenticates using the current user's session if logged in
paths:
"/api/v1/customer_account_transaction":
post:
summary: Create customer transaction
tags:
- Customer account transaction
parameters: []
responses:
'201':
description: Customer transaction created
content:
application/json:
schema:
"$ref": "#/components/schemas/customer_account_transaction"
'422':
description: Invalid resource
content:
application/json:
schema:
"$ref": "#/components/schemas/error_response"
'401':
description: Unauthorized
requestBody:
content:
application/json:
schema:
type: object
properties:
customer_id:
type: integer
example: 10
amount:
type: decimal
example: 10.5
description:
type: string
nullable: true
example: Payment processed by POS
required:
- customer_id
- amount
"/api/v1/customers":
get:
summary: List customers
@@ -375,7 +501,7 @@ paths:
billing_address:
type: object
nullable: true
example:
example:
shipping_address:
type: object
nullable: true
@@ -468,7 +594,7 @@ paths:
billing_address:
type: object
nullable: true
example:
example:
shipping_address:
type: object
nullable: true
@@ -598,7 +724,7 @@ paths:
billing_address:
type: object
nullable: true
example:
example:
shipping_address:
type: object
nullable: true