mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
feat(api): add transaction idempotency keys (#1729)
* feat(api): add transaction idempotency keys * fix(api): validate transaction idempotency source * fix(api): tighten transaction idempotency params
This commit is contained in:
@@ -69,7 +69,7 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||
family = current_resource_owner.family
|
||||
|
||||
# Validate account_id is present
|
||||
unless transaction_params[:account_id].present?
|
||||
unless account_id_param.present?
|
||||
render json: {
|
||||
error: "validation_failed",
|
||||
message: "Account ID is required",
|
||||
@@ -78,7 +78,21 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||
return
|
||||
end
|
||||
|
||||
account = family.accounts.writable_by(current_resource_owner).find(transaction_params[:account_id])
|
||||
if idempotency_source_param.present? && idempotency_external_id.blank?
|
||||
render json: {
|
||||
error: "validation_failed",
|
||||
message: "Source requires external_id",
|
||||
errors: [ "Source requires external_id" ]
|
||||
}, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
account = family.accounts.writable_by(current_resource_owner).find(account_id_param)
|
||||
|
||||
if idempotency_key_requested? && (existing_entry = existing_idempotent_entry(account))
|
||||
return render_existing_idempotent_entry(existing_entry)
|
||||
end
|
||||
|
||||
@entry = account.entries.new(entry_params_for_create)
|
||||
|
||||
if @entry.save
|
||||
@@ -96,6 +110,12 @@ class Api::V1::TransactionsController < Api::V1::BaseController
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
if idempotency_key_requested? && account && (existing_entry = existing_idempotent_entry(account))
|
||||
render_existing_idempotent_entry(existing_entry)
|
||||
else
|
||||
raise
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "TransactionsController#create error: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
@@ -282,11 +302,15 @@ end
|
||||
|
||||
def transaction_params
|
||||
params.require(:transaction).permit(
|
||||
:account_id, :date, :amount, :name, :description, :notes, :currency,
|
||||
:date, :amount, :name, :description, :notes, :currency,
|
||||
:category_id, :merchant_id, :nature, tag_ids: []
|
||||
)
|
||||
end
|
||||
|
||||
def account_id_param
|
||||
params.dig(:transaction, :account_id).presence
|
||||
end
|
||||
|
||||
def entry_params_for_create
|
||||
entry_params = {
|
||||
name: transaction_params[:name] || transaction_params[:description],
|
||||
@@ -301,6 +325,10 @@ end
|
||||
tag_ids: transaction_params[:tag_ids] || []
|
||||
}
|
||||
}
|
||||
if idempotency_key_requested?
|
||||
entry_params[:external_id] = idempotency_external_id
|
||||
entry_params[:source] = idempotency_source
|
||||
end
|
||||
|
||||
entry_params.compact
|
||||
end
|
||||
@@ -339,6 +367,49 @@ end
|
||||
params.dig(:transaction, :nature).present?
|
||||
end
|
||||
|
||||
def idempotency_key_requested?
|
||||
idempotency_external_id.present?
|
||||
end
|
||||
|
||||
def idempotency_external_id
|
||||
idempotency_param_value(:external_id)
|
||||
end
|
||||
|
||||
def idempotency_source
|
||||
idempotency_source_param.presence || "api"
|
||||
end
|
||||
|
||||
def idempotency_source_param
|
||||
idempotency_param_value(:source)
|
||||
end
|
||||
|
||||
def idempotency_param_value(key)
|
||||
value = params.dig(:transaction, key)
|
||||
value.to_s.presence if value.is_a?(String) || value.is_a?(Numeric)
|
||||
end
|
||||
|
||||
def existing_idempotent_entry(account)
|
||||
account.entries.find_by(
|
||||
external_id: idempotency_external_id,
|
||||
source: idempotency_source
|
||||
)
|
||||
end
|
||||
|
||||
def render_existing_idempotent_entry(entry)
|
||||
unless entry.entryable.is_a?(Transaction)
|
||||
render json: {
|
||||
error: "validation_failed",
|
||||
message: "External ID already exists for a non-transaction entry",
|
||||
errors: [ "External ID already exists for a non-transaction entry" ]
|
||||
}, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
@entry = entry
|
||||
@transaction = entry.transaction
|
||||
render :show, status: :ok
|
||||
end
|
||||
|
||||
def calculate_signed_amount
|
||||
amount = transaction_params[:amount].to_f
|
||||
nature = transaction_params[:nature]
|
||||
|
||||
Reference in New Issue
Block a user