Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Storefront API Checkout Create Payment endpoint #11436

Merged
merged 15 commits into from
Nov 1, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ def model_class
end

def scope
super.where(user: spree_current_user, payment_method: current_store.payment_methods.available_on_front_end)
super.not_expired.not_removed.where(
user: spree_current_user,
payment_method: current_store.payment_methods.available_on_front_end
)
end

def collection_serializer
Expand Down
14 changes: 14 additions & 0 deletions api/app/controllers/spree/api/v2/storefront/checkout_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ def update
render_order(result)
end

def create_payment
result = create_payment_service.call(order: spree_current_order, params: params)

if result.success?
render_serialized_payload(201) { serialize_resource(spree_current_order.reload) }
else
render_error_payload(result.error)
end
end

def add_store_credit
spree_authorize! :update, spree_current_order, order_token

Expand Down Expand Up @@ -118,6 +128,10 @@ def shipping_rates_serializer
Spree::Api::Dependencies.storefront_shipment_serializer.constantize
end

def create_payment_service
Spree::Api::Dependencies.storefront_payment_create_service.constantize
end

def serialize_payment_methods(payment_methods)
payment_methods_serializer.new(payment_methods, params: serializer_params).serializable_hash
end
Expand Down
1 change: 1 addition & 0 deletions api/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
patch :next
patch :advance
patch :complete
post :create_payment
post :add_store_credit
post :remove_store_credit
get :payment_methods
Expand Down
116 changes: 102 additions & 14 deletions api/docs/v2/storefront/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -730,13 +730,13 @@ paths:
source_attributes:
type: object
properties:
number:
gateway_payment_profile_id:
type: string
month:
type: string
year:
type: string
verification_value:
cc_type:
type: string
name:
type: string
Expand Down Expand Up @@ -778,10 +778,11 @@ paths:
payments_attributes:
- payment_method_id: '1'
source_attributes:
number: '4111111111111111'
gateway_payment_profile_id: 'ABC123'
cc_type: 'visa'
last_digits: '1111'
month: '10'
year: '2022'
verification_value: '123'
name: John Doe
description: |-
##### Add Customer Details
Expand Down Expand Up @@ -867,6 +868,92 @@ paths:
summary: Complete Checkout
tags:
- Checkout / State
/api/v2/storefront/checkout/create_payment:
post:
description: |-
Creates new Payment for the current checkout.

You can either create new payment source (eg. a Credit Card) or use an existing (for signed in users only).

Newly created payment source will be associated to the current signed in user.

System will automatically invalidate previous (non-finalized) payments (excluding store credit / gift card payments).

[More details on payment system](https://dev-docs.spreecommerce.org/internals/payments)
operationId: create-payment
responses:
'200':
$ref: '#/components/responses/Cart'
'404':
$ref: '#/components/responses/404NotFound'
'422':
$ref: '#/components/responses/422UnprocessableEntity'
security:
- orderToken: []
- bearerAuth: []
requestBody:
content:
application/vnd.api+json:
schema:
type: object
properties:
payment_method_id:
type: string
required: true
description: ID of the selected Payment Method
source_id:
type: string
description: ID of the selected Payment Source (eg. Credit Card, only for signed in users)
amount:
type: number
description: Amount for the newly created payment, when left blank will use the entire Order Total (reduced by used Store Credits / Gift Cards)
source_attributes:
type: object
properties:
gateway_payment_profile_id:
type: string
description: Payment source authorization token, more details for [Stripe implementation](https://stripe.com/docs/payments/accept-a-payment-charges#web-create-token)
required: true
cc_type:
type: string
enum:
- visa
- mastercard
- amex
last_digits:
type: string
description: Last 4 digits of CC number
month:
type: string
description: Expiration date month
year:
type: string
description: Expiration date year
name:
type: string
description: Card holder name
required: true
examples:
Create new Credit Card:
value:
payment_method_id: '1'
source_attributes:
gateway_payment_profile_id: 'card_1JqvNB2eZvKYlo2C5OlqLV7S'
cc_type: 'visa'
last_digits: '1111'
month: '10'
year: '2026'
name: 'John Snow'
Use existing Credit Card:
value:
payment_metod: '1'
source_id: '1'
parameters:
- $ref: '#/components/parameters/CartIncludeParam'
- $ref: '#/components/parameters/SparseFieldsCart'
summary: Create new Payment
tags:
- Checkout / Payments
/api/v2/storefront/checkout/add_store_credit:
post:
description: |-
Expand Down Expand Up @@ -2230,15 +2317,15 @@ components:
example: visa
last_digits:
type: string
example: '1232'
example: '1111'
description: Last 4 digits of CC number
month:
type: string
description: Expiration date month
example: '10'
example: '12'
year:
type: string
example: '2019'
example: '2026'
description: Expiration date year
name:
type: string
Expand Down Expand Up @@ -3022,7 +3109,7 @@ components:
shipped_at:
type: string
format: date-time
example: '2019-01-02 13:42:12'
example: '2021-01-02 13:42:12'
description: Date when Shipment was being sent from the warehouse
nullable: true
relationships:
Expand Down Expand Up @@ -5865,9 +5952,9 @@ components:
type: credit_card
attributes:
cc_type: visa
last_digits: '1232'
month: '10'
year: '2019'
last_digits: '4111'
month: '12'
year: '2026'
name: John Doe
default: true
relationships:
Expand Down Expand Up @@ -5914,9 +6001,9 @@ components:
type: credit_card
attributes:
cc_type: visa
last_digits: '1232'
month: '10'
year: '2019'
last_digits: '1111'
month: '12'
year: '2026'
name: John Doe
default: true
relationships:
Expand Down Expand Up @@ -15993,6 +16080,7 @@ tags:
- name: Cart / Other
- name: Checkout
- name: Checkout / State
- name: Checkout / Payments
- name: Checkout / Store Credit
- name: CMS Pages
- name: Countries
Expand Down
9 changes: 7 additions & 2 deletions api/lib/spree/api/api_dependencies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class ApiDependencies
:storefront_account_create_address_service, :storefront_account_update_address_service, :storefront_address_finder,
:storefront_account_create_service, :storefront_account_update_service, :storefront_collection_sorter, :error_handler,
:storefront_cart_empty_service, :storefront_cart_destroy_service, :storefront_credit_cards_destroy_service, :platform_products_sorter,
:storefront_cart_change_currency_service,
:storefront_cart_change_currency_service, :storefront_payment_serializer,
:storefront_payment_create_service,

:platform_admin_user_serializer, :platform_coupon_handler, :platform_order_update_service,
:platform_order_use_store_credit_service, :platform_order_remove_store_credit_service,
Expand Down Expand Up @@ -74,9 +75,12 @@ def set_storefront_defaults
@storefront_account_create_address_service = Spree::Dependencies.account_create_address_service
@storefront_account_update_address_service = Spree::Dependencies.account_update_address_service

# credit cards
# credit card services
@storefront_credit_cards_destroy_service = Spree::Dependencies.credit_cards_destroy_service

# payment services
@storefront_payment_create_service = Spree::Dependencies.payment_create_service

# serializers
@storefront_address_serializer = 'Spree::V2::Storefront::AddressSerializer'
@storefront_cart_serializer = 'Spree::V2::Storefront::CartSerializer'
Expand All @@ -88,6 +92,7 @@ def set_storefront_defaults
@storefront_shipment_serializer = 'Spree::V2::Storefront::ShipmentSerializer'
@storefront_taxon_serializer = 'Spree::V2::Storefront::TaxonSerializer'
@storefront_payment_method_serializer = 'Spree::V2::Storefront::PaymentMethodSerializer'
@storefront_payment_serializer = 'Spree::V2::Storefront::PaymentSerializer'
@storefront_product_serializer = 'Spree::V2::Storefront::ProductSerializer'
@storefront_estimated_shipment_serializer = 'Spree::V2::Storefront::EstimatedShippingRateSerializer'
@storefront_store_serializer = 'Spree::V2::Storefront::StoreSerializer'
Expand Down
105 changes: 102 additions & 3 deletions api/spec/requests/spree/api/v2/storefront/checkout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@

let(:payment_source_attributes) do
{
number: '4111111111111111',
gateway_payment_profile_id: 'BGS-123',
gateway_customer_profile_id: 'BGS-123',
month: 1.month.from_now.month,
year: 1.month.from_now.year,
verification_value: '123',
name: 'Spree Commerce'
name: 'Spree Commerce',
last_digits: '1111'
}
end
let(:payment_params) do
Expand Down Expand Up @@ -581,6 +582,104 @@
end
end

describe 'checkout#create_payment' do
let(:params) do
{
payment_method_id: payment_method.id,
amount: order.total,
source_attributes: {
gateway_payment_profile_id: '12345',
cc_type: 'visa',
last_digits: '1111',
name: 'John',
month: '12',
year: '2021'
}
}
end
let(:execute) { post '/api/v2/storefront/checkout/create_payment?include=payments', headers: headers, params: params }
let!(:payment_method) { create(:credit_card_payment_method, stores: [store]) }
let(:payment_source) { payment_method.payment_source_class.last }
let(:payment) { order.payments.last }

shared_examples 'creates a payment' do
before { execute }

it_behaves_like 'returns 201 HTTP status'

it 'returns new payment' do
expect(json_response['data']).to have_relationship(:payments).with_data([{ 'id' => payment.id.to_s, 'type' => 'payment' }])
expect(json_response['included'][0]).to have_id(payment.id.to_s)
expect(json_response['included'][0]).to have_type('payment')
expect(json_response['included'][0]).to have_attribute(:amount).with_value(order.total.to_s)
expect(json_response['included'][0]).to have_jsonapi_attributes(:amount, :response_code, :number, :cvv_response_code, :cvv_response_message, :payment_method_id, :payment_method_name, :state)
expect(json_response['included'][0]).to have_relationship(:payment_method).with_data({ 'id' => payment_method.id.to_s, 'type' => 'payment_method' })
end

it 'creates new payment record' do
expect { change(order.payments, :count).by(1) }
end
end

shared_examples 'creates a payment source' do
let(:execute) { post '/api/v2/storefront/checkout/create_payment?include=payments.source', headers: headers, params: params }

before { execute }

it 'returns new payment source' do
expect(json_response['included'][0]).to have_id(payment_source.id.to_s)
expect(json_response['included'][0]).to have_type('credit_card')
expect(json_response['included'][0]).to have_relationship(:payment_method).with_data({ 'id' => payment_method.id.to_s, 'type' => 'payment_method' })
expect(json_response['included'][0]).to have_attribute(:last_digits).with_value('1111')
expect(json_response['included'][0]).to have_attribute(:name).with_value('John')
expect(json_response['included'][0]).to have_attribute(:year).with_value(2021)
expect(json_response['included'][0]).to have_attribute(:month).with_value(12)
end

it 'creates new payment source record' do
expect { change(payment_method.payment_source_class, :count).by(1) }
end
end

context 'as a guest user' do
include_context 'creates guest order with guest token'

it_behaves_like 'creates a payment'
it_behaves_like 'creates a payment source'
end

context 'as a signed in user' do
include_context 'order with a physical line item'

context 'new payment source' do
it_behaves_like 'creates a payment'
it_behaves_like 'creates a payment source'

it 'assigns new payment source to the signed in user' do
expect { execute }.to change(user.credit_cards, :count).by(1)
expect(payment_source.user).to eq(user)
end
end

context 'existing payment source' do
let!(:payment_source) { create(:credit_card, user: user, payment_method: payment_method) }

let(:params) do
{
payment_method_id: payment_method.id,
source_id: payment_source.id
}
end

it_behaves_like 'creates a payment'

it 'does not create new payment source record' do
expect { execute }.not_to change(payment_method.payment_source_class, :count)
end
end
end
end

describe 'checkout#shipping_rates' do
let(:execute) { get '/api/v2/storefront/checkout/shipping_rates', headers: headers }

Expand Down
4 changes: 4 additions & 0 deletions core/app/models/spree/gateway/bogus.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def payment_profiles_supported?
true
end

def payment_source_class
CreditCard
end

def actions
%w(capture void credit)
end
Expand Down