1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

Merge branch 'dev' for release 5.5.5

This commit is contained in:
Du Peng 2022-11-22 17:20:07 +01:00
commit 71409395e2
33 changed files with 746 additions and 261 deletions

View File

@ -1,5 +1,14 @@
# Changelog Fab-manager
## v5.5.5 2022 November 22
- Soft destroy of spaces and machines
- Fix a bug: in upgrade script, the error "the input device is not a TTY" is thrown when migrating the database
- Fix a bug: broken display of machines pages
- Fix a bug: some automated tests were randomly failing because ElasticSearch was not synced
- Fix a bug: payment related objects are not synced on Stripe when enabling the online payment module
- Fix a bug: unable set a main image of product and remove an image of product
## v5.5.4 2022 November 17
- Fix a bug: unable to download an existing export of the statistics

View File

@ -12,6 +12,8 @@ class API::MachinesController < API::ApiController
def show
@machine = Machine.includes(:machine_files, :projects).friendly.find(params[:id])
head :not_found if @machine.deleted_at
end
def create
@ -35,7 +37,11 @@ class API::MachinesController < API::ApiController
def destroy
authorize @machine
@machine.destroy
if @machine.destroyable?
@machine.destroy
else
@machine.soft_destroy!
end
head :no_content
end

View File

@ -6,11 +6,13 @@ class API::SpacesController < API::ApiController
respond_to :json
def index
@spaces = Space.includes(:space_image)
@spaces = Space.includes(:space_image).where(deleted_at: nil)
end
def show
@space = Space.includes(:space_files, :projects).friendly.find(params[:id])
head :not_found if @space.deleted_at
end
def create
@ -36,7 +38,11 @@ class API::SpacesController < API::ApiController
def destroy
@space = get_space
authorize @space
@space.destroy
if @space.destroyable?
@space.destroy
else
@space.soft_destroy!
end
head :no_content
end

View File

@ -60,11 +60,12 @@ export const FormImageUpload = <TFieldValues extends FieldValues, TContext exten
attachment_name: f.name
});
setValue(
id as Path<TFieldValues>,
{
attachment_name: f.name,
_destroy: false
} as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
`${id}.attachment_name` as Path<TFieldValues>,
f.name as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
setValue(
`${id}._destroy` as Path<TFieldValues>,
false as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
if (typeof onFileChange === 'function') {
onFileChange({ attachment_name: f.name });

View File

@ -6,9 +6,10 @@ import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FormComponent, FormControlledComponent } from '../../models/form-component';
import { AbstractFormItemProps } from './abstract-form-item';
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
import { ArrayPath, FieldArray, useFieldArray } from 'react-hook-form';
import { ArrayPath, FieldArray, Path, useFieldArray, useWatch } from 'react-hook-form';
import { FileType } from '../../models/file';
import { UnpackNestedValue } from 'react-hook-form/dist/types';
import { FieldPathValue } from 'react-hook-form/dist/types/path';
interface FormMultiFileUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
setValue: UseFormSetValue<TFieldValues>,
@ -20,13 +21,26 @@ interface FormMultiFileUploadProps<TFieldValues, TContext extends object> extend
* This component allows to upload multiple files, in forms managed by react-hook-form.
*/
export const FormMultiFileUpload = <TFieldValues extends FieldValues, TContext extends object>({ id, className, register, control, setValue, formState, addButtonLabel, accept }: FormMultiFileUploadProps<TFieldValues, TContext>) => {
const { fields, append, remove } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
const { append } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
const output = useWatch({ control, name: id as Path<TFieldValues> });
/**
* Remove an file
*/
const handleRemoveFile = (file: FileType, index: number) => {
return () => {
setValue(
`${id}.${index}._destroy` as Path<TFieldValues>,
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
};
};
return (
<div className={`form-multi-file-upload ${className || ''}`}>
<div className="list">
{fields.map((field: FileType, index) => (
<FormFileUpload key={field.id}
{output.map((field: FileType, index) => (
<FormFileUpload key={index}
defaultFile={field}
id={`${id}.${index}`}
accept={accept}
@ -34,7 +48,7 @@ export const FormMultiFileUpload = <TFieldValues extends FieldValues, TContext e
setValue={setValue}
formState={formState}
className={field._destroy ? 'hidden' : ''}
onFileRemove={() => remove(index)}/>
onFileRemove={() => handleRemoveFile(field, index)}/>
))}
</div>
<FabButton

View File

@ -20,7 +20,7 @@ interface FormMultiImageUploadProps<TFieldValues, TContext extends object> exten
* This component allows to upload multiple images, in forms managed by react-hook-form.
*/
export const FormMultiImageUpload = <TFieldValues extends FieldValues, TContext extends object>({ id, className, register, control, setValue, formState, addButtonLabel }: FormMultiImageUploadProps<TFieldValues, TContext>) => {
const { fields, append, remove } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
const { append } = useFieldArray({ control, name: id as ArrayPath<TFieldValues> });
const output = useWatch({ control, name: id as Path<TFieldValues> });
/**
@ -44,14 +44,10 @@ export const FormMultiImageUpload = <TFieldValues extends FieldValues, TContext
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
}
if (typeof image.id === 'string') {
remove(index);
} else {
setValue(
`${id}.${index}._destroy` as Path<TFieldValues>,
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
}
setValue(
`${id}.${index}._destroy` as Path<TFieldValues>,
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
};
};
@ -74,8 +70,8 @@ export const FormMultiImageUpload = <TFieldValues extends FieldValues, TContext
return (
<div className={`form-multi-image-upload ${className || ''}`}>
<div className="list">
{fields.map((field: ImageType, index) => (
<FormImageUpload key={field.id}
{output.map((field: ImageType, index) => (
<FormImageUpload key={index}
defaultImage={field}
id={`${id}.${index}`}
accept="image/*"

View File

@ -44,12 +44,12 @@ const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine,
* Return the machine's picture or a placeholder
*/
const machinePicture = (): ReactNode => {
if (!machine.machine_image_attributes) {
if (!machine.machine_image) {
return <div className="machine-picture no-picture" />;
}
return (
<div className="machine-picture" style={{ backgroundImage: `url(${machine.machine_image_attributes.attachment_url}), url('/default-image.png')` }} onClick={handleShowMachine} />
<div className="machine-picture" style={{ backgroundImage: `url(${machine.machine_image}), url('/default-image.png')` }} onClick={handleShowMachine} />
);
};

View File

@ -1,6 +1,5 @@
import { Reservation } from './reservation';
import { ApiFilter } from './api';
import { FileType } from './file';
export interface MachineIndexFilter extends ApiFilter {
disabled: boolean,
@ -13,8 +12,12 @@ export interface Machine {
spec?: string,
disabled: boolean,
slug: string,
machine_image_attributes: FileType,
machine_files_attributes?: Array<FileType>,
machine_image: string,
machine_files_attributes?: Array<{
id: number,
attachment: string,
attachment_url: string
}>,
trainings?: Array<{
id: number,
name: string,

View File

@ -82,6 +82,10 @@ class Machine < ApplicationRecord
reservations.empty?
end
def soft_destroy!
update(deleted_at: DateTime.current)
end
def packs?(user)
prepaid_packs.where(group_id: user.group_id)
.where(disabled: [false, nil])

View File

@ -15,7 +15,7 @@ class Product < ApplicationRecord
accepts_nested_attributes_for :product_files, allow_destroy: true, reject_if: :all_blank
has_many :product_images, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :product_images, allow_destroy: true, reject_if: ->(i) { i[:attachment].blank? }
accepts_nested_attributes_for :product_images, allow_destroy: true, reject_if: ->(i) { i[:attachment].blank? && i[:id].blank? }
has_many :product_stock_movements, dependent: :destroy
accepts_nested_attributes_for :product_stock_movements, allow_destroy: true, reject_if: :all_blank

View File

@ -66,6 +66,10 @@ class Space < ApplicationRecord
reservations.empty?
end
def soft_destroy!
update(deleted_at: DateTime.current)
end
private
def update_gateway_product

View File

@ -11,6 +11,6 @@ class MachinePolicy < ApplicationPolicy
end
def destroy?
user.admin? and record.destroyable?
user.admin?
end
end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Check the access policies for API::SpacesController
class SpacePolicy < ApplicationPolicy
def create?
user.admin?
@ -8,6 +11,6 @@ class SpacePolicy < ApplicationPolicy
end
def destroy?
user.admin? and record.destroyable?
user.admin?
end
end

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# module definition
module Cart; end
# Provides methods for set offer to item in cart
class Cart::SetOfferService
def call(order, orderable, is_offered)

View File

@ -9,6 +9,9 @@ class MachineService
else
Machine.includes(:machine_image, :plans).order(sort_by)
end
# do not include soft destroyed
machines = machines.where(deleted_at: nil)
if filters[:disabled].present?
state = filters[:disabled] == 'false' ? [nil, false] : true
machines = machines.where(disabled: state)

View File

@ -46,7 +46,7 @@ class SettingService
# sync all objects on stripe
def sync_stripe_objects(setting)
return unless setting.name == 'stripe_secret_key'
return unless %w[stripe_secret_key online_payment_module].include?(setting.name)
SyncObjectsOnStripeWorker.perform_async(setting.history_values.last&.invoicing_profile&.user&.id)
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
json.array!(@machines) do |machine|
json.partial! 'api/machines/machine', machine: machine
json.extract! machine, :id, :name, :slug, :disabled
json.machine_image machine.machine_image.attachment.medium.url if machine.machine_image
end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
json.partial! 'api/machines/machine', machine: @machine
json.extract! @machine, :description, :spec
json.extract! @machine, :id, :name, :description, :spec, :disabled, :slug
json.machine_image @machine.machine_image.attachment.large.url if @machine.machine_image
json.machine_files_attributes @machine.machine_files do |f|
json.id f.id
json.attachment_name f.attachment_identifier
json.attachment f.attachment_identifier
json.attachment_url f.attachment_url
end
json.trainings @machine.trainings.each, :id, :name, :disabled

View File

@ -4,12 +4,12 @@ json.extract! product, :id, :name, :slug, :sku, :is_active, :product_category_id
:low_stock_threshold, :machine_ids, :created_at
json.description sanitize(product.description)
json.amount product.amount / 100.0 if product.amount.present?
json.product_files_attributes product.product_files do |f|
json.product_files_attributes product.product_files.order(created_at: :asc) do |f|
json.id f.id
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url
end
json.product_images_attributes product.product_images do |f|
json.product_images_attributes product.product_images.order(created_at: :asc) do |f|
json.id f.id
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url

View File

@ -7,22 +7,45 @@ class SyncObjectsOnStripeWorker
sidekiq_options lock: :until_executed, on_conflict: :reject, queue: :stripe
def perform(notify_user_id = nil)
unless Stripe::Helper.enabled?
Rails.logger.warn('A request to sync payment related objects on Stripe will not run because Stripe is not enabled')
return false
end
w = StripeWorker.new
sync_customers(w)
sync_coupons
sync_objects(w)
Rails.logger.info 'Sync is done'
return unless notify_user_id
Rails.logger.info "Notify user #{notify_user_id}"
user = User.find(notify_user_id)
NotificationCenter.call type: :notify_admin_objects_stripe_sync,
receiver: user,
attached_object: user
end
def sync_customers(worker)
Rails.logger.info 'We create all non-existing customers on stripe. This may take a while...'
total = User.online_payers.count
User.online_payers.each_with_index do |member, index|
Rails.logger.info "#{index} / #{total}"
begin
stp_customer = member.payment_gateway_object&.gateway_object&.retrieve
StripeWorker.new.create_stripe_customer(member.id) if stp_customer.nil? || stp_customer[:deleted]
worker.perform(:create_stripe_customer, member.id) if stp_customer.nil? || stp_customer[:deleted]
rescue Stripe::InvalidRequestError
begin
StripeWorker.new.create_stripe_customer(member.id)
worker.perform(:create_stripe_customer, member.id)
rescue Stripe::InvalidRequestError => e
Rails.logger.error "Unable to create the customer #{member.id} do to a Stripe error: #{e}"
end
end
end
end
def sync_coupons
Rails.logger.info 'We create all non-existing coupons on stripe. This may take a while...'
total = Coupon.all.count
Coupon.all.each_with_index do |coupon, index|
@ -35,23 +58,16 @@ class SyncObjectsOnStripeWorker
Rails.logger.error "Unable to create coupon #{coupon.code} on stripe: #{e}"
end
end
end
w = StripeWorker.new
def sync_objects(worker)
[Machine, Training, Space, Plan].each do |klass|
Rails.logger.info "We create all non-existing #{klass} on stripe. This may take a while..."
total = klass.all.count
klass.all.each_with_index do |item, index|
Rails.logger.info "#{index} / #{total}"
w.perform(:create_or_update_stp_product, klass.name, item.id)
worker.perform(:create_or_update_stp_product, klass.name, item.id)
end
end
Rails.logger.info 'Sync is done'
return unless notify_user_id
Rails.logger.info "Notify user #{notify_user_id}"
user = User.find(notify_user_id)
NotificationCenter.call type: :notify_admin_objects_stripe_sync,
receiver: user,
attached_object: user
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Allow soft destroy of machines.
# Machines with existing reservation cannot be destroyed because we need them for rebuilding invoices, statistics, etc.
# This attribute allows to make a "soft destroy" of a Machine, marking it as destroyed so it doesn't appear anymore in
# the interface (as if it was destroyed) but still lives in the database so we can use it to build data.
class AddDeletedAtToMachine < ActiveRecord::Migration[5.2]
def change
add_column :machines, :deleted_at, :datetime
add_index :machines, :deleted_at
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Allow soft destroy of spaces.
# Spaces with existing reservation cannot be destroyed because we need them for rebuilding invoices, statistics, etc.
# This attribute allows to make a "soft destroy" of a Space, marking it as destroyed so it doesn't appear anymore in
# the interface (as if it was destroyed) but still lives in the database so we can use it to build data.
class AddDeletedAtToSpace < ActiveRecord::Migration[5.2]
def change
add_column :spaces, :deleted_at, :datetime
add_index :spaces, :deleted_at
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_10_03_133019) do
ActiveRecord::Schema.define(version: 2022_11_22_123605) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"
@ -358,6 +358,8 @@ ActiveRecord::Schema.define(version: 2022_10_03_133019) do
t.datetime "updated_at"
t.string "slug"
t.boolean "disabled"
t.datetime "deleted_at"
t.index ["deleted_at"], name: "index_machines_on_deleted_at"
t.index ["slug"], name: "index_machines_on_slug", unique: true
end
@ -878,6 +880,8 @@ ActiveRecord::Schema.define(version: 2022_10_03_133019) do
t.datetime "updated_at", null: false
t.text "characteristics"
t.boolean "disabled"
t.datetime "deleted_at"
t.index ["deleted_at"], name: "index_spaces_on_deleted_at"
end
create_table "spaces_availabilities", id: :serial, force: :cascade do |t|

View File

@ -21,3 +21,12 @@ to the corresponding OpenID Connect claims:
- profile.address
To use the automatic mapping, add one of the fields above and click on the magic wand button near to the "Userinfo claim" input.
## Known issues
```
Not found. Authentication passthru.
```
This issue may occur if you have misconfigured the environment variable `DEFAULT_HOST` and/or `DEFAULT_PROTOCOL`.
Especially, if you have an automatic redirection (e.g. from example.org to example.com), `DEFAULT_HOST` *MUST* be configured with the redirection target (here example.com).
Once you have reconfigured these variables, please switch back the active authentication provider to FabManager, restart the application, then delete the OIDC provider you configured and re-create a new one for the new settings to be used.

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.5.4",
"version": "5.5.5",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -296,7 +296,7 @@ upgrade()
fi
done
compile_assets
if ! docker-compose run --rm "$SERVICE" bundle exec rake db:migrate; then
if ! docker-compose run --rm "$SERVICE" bundle exec rake db:migrate </dev/tty; then
restore_tag
printf "\e[91m[ ❌ ] Something went wrong while migrating the database, please check the logs above.\e[39m\nExiting...\n"
exit 4

View File

@ -12,6 +12,7 @@ class Exports::StatisticsExportTest < ActionDispatch::IntegrationTest
end
test 'export machine reservations statistics to Excel' do
Stats::Machine.refresh_index!
# Build the stats for the June 2015, a machine reservation should have happened at the time
::Statistics::BuilderService.generate_statistic({ start_date: '2015-06-01'.to_date.beginning_of_day,
end_date: '2015-06-30'.to_date.end_of_day })
@ -35,6 +36,7 @@ class Exports::StatisticsExportTest < ActionDispatch::IntegrationTest
assert_not_nil e, 'Export was not created in database'
# Run the worker
Stats::Machine.refresh_index!
worker = StatisticsExportWorker.new
worker.perform(e.id)
@ -69,6 +71,7 @@ class Exports::StatisticsExportTest < ActionDispatch::IntegrationTest
end
test 'export global statistics to Excel' do
Stats::Machine.refresh_index!
# Build the stats for the June 2015
::Statistics::BuilderService.generate_statistic({ start_date: '2015-06-01'.to_date.beginning_of_day,
end_date: '2015-06-30'.to_date.end_of_day })

View File

@ -0,0 +1,209 @@
# frozen_string_literal: true
require 'test_helper'
module Store; end
class Store::AdminOrderForHimselfTest < ActionDispatch::IntegrationTest
setup do
@admin = User.find_by(username: 'admin')
@pjproudhon = User.find_by(username: 'pjproudhon')
@caisse_en_bois = Product.find_by(slug: 'caisse-en-bois')
@panneaux = Product.find_by(slug: 'panneaux-de-mdf')
@cart1 = Order.find_by(token: '0DKxbAOzSXRx-amXyhmDdg1666691976019')
end
test 'admin pay himself order by card with success' do
login_as(@admin, scope: :user)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('store_order_admin_pay_by_card_success') do
post '/api/checkout/payment',
params: {
payment_id: stripe_payment_method,
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
end
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not @cart1.payment_gateway_object.blank?
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
assert_equal @cart1.state, 'paid'
assert_equal @cart1.payment_method, 'card'
assert_equal @cart1.paid_total, 262_500
stock_movement = @caisse_en_bois.product_stock_movements.last
assert_equal stock_movement.stock_type, 'external'
assert_equal stock_movement.reason, 'sold'
assert_equal stock_movement.quantity, -5
assert_equal stock_movement.order_item_id, @cart1.order_items.first.id
stock_movement = @panneaux.product_stock_movements.last
assert_equal stock_movement.stock_type, 'external'
assert_equal stock_movement.reason, 'sold'
assert_equal stock_movement.quantity, -2
assert_equal stock_movement.order_item_id, @cart1.order_items.last.id
activity = @cart1.order_activities.last
assert_equal activity.activity_type, 'paid'
assert_equal activity.operator_profile_id, @admin.invoicing_profile.id
end
test 'admin pay himself order by card and wallet with success' do
login_as(@admin, scope: :user)
service = WalletService.new(user: @admin, wallet: @admin.wallet)
service.credit(1000)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('store_order_admin_pay_by_cart_and_wallet_success') do
post '/api/checkout/payment',
params: {
payment_id: stripe_payment_method,
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
end
@admin.wallet.reload
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not @cart1.payment_gateway_object.blank?
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
assert_equal @cart1.state, 'paid'
assert_equal @cart1.payment_method, 'card'
assert_equal @cart1.paid_total, 162_500
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# wallet
assert_equal 0, @admin.wallet.amount
assert_equal 2, @admin.wallet.wallet_transactions.count
transaction = @admin.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal @cart1.wallet_amount / 100.0, transaction.amount
assert_equal @cart1.wallet_transaction_id, transaction.id
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'admin pay himself order by wallet with success' do
login_as(@admin, scope: :user)
service = WalletService.new(user: @admin, wallet: @admin.wallet)
service.credit(@cart1.total / 100)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
post '/api/checkout/payment',
params: {
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
@admin.wallet.reload
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal @cart1.state, 'paid'
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
# wallet
assert_equal 0, @admin.wallet.amount
assert_equal 2, @admin.wallet.wallet_transactions.count
transaction = @admin.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal @cart1.paid_total, 0
assert_equal @cart1.wallet_amount / 100.0, transaction.amount
assert_equal @cart1.payment_method, 'wallet'
assert_equal @cart1.wallet_transaction_id, transaction.id
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'admin cannot offer products to himself' do
login_as(@admin, scope: :user)
put '/api/cart/set_offer',
params: {
order_token: @cart1.token,
customer_id: @admin.id,
is_offered: true,
orderable_id: @caisse_en_bois.id
}.to_json, headers: default_headers
assert_equal 403, response.status
end
end

View File

@ -13,129 +13,6 @@ class Store::AdminPayOrderTest < ActionDispatch::IntegrationTest
@cart1 = Order.find_by(token: '0DKxbAOzSXRx-amXyhmDdg1666691976019')
end
test 'admin pay himself order by cart with success' do
login_as(@admin, scope: :user)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
VCR.use_cassette('store_order_admin_pay_by_cart_success') do
post '/api/checkout/payment',
params: {
payment_id: stripe_payment_method,
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
end
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not @cart1.payment_gateway_object.blank?
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
assert_equal @cart1.state, 'paid'
assert_equal @cart1.payment_method, 'card'
assert_equal @cart1.paid_total, 262_500
stock_movement = @caisse_en_bois.product_stock_movements.last
assert_equal stock_movement.stock_type, 'external'
assert_equal stock_movement.reason, 'sold'
assert_equal stock_movement.quantity, -5
assert_equal stock_movement.order_item_id, @cart1.order_items.first.id
stock_movement = @panneaux.product_stock_movements.last
assert_equal stock_movement.stock_type, 'external'
assert_equal stock_movement.reason, 'sold'
assert_equal stock_movement.quantity, -2
assert_equal stock_movement.order_item_id, @cart1.order_items.last.id
activity = @cart1.order_activities.last
assert_equal activity.activity_type, 'paid'
assert_equal activity.operator_profile_id, @admin.invoicing_profile.id
end
test 'admin pay himself order by cart and wallet with success' do
login_as(@admin, scope: :user)
service = WalletService.new(user: @admin, wallet: @admin.wallet)
service.credit(1000)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
VCR.use_cassette('store_order_admin_pay_by_cart_and_wallet_success') do
post '/api/checkout/payment',
params: {
payment_id: stripe_payment_method,
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
end
@admin.wallet.reload
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert_not @cart1.payment_gateway_object.blank?
assert_not invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
assert_equal @cart1.state, 'paid'
assert_equal @cart1.payment_method, 'card'
assert_equal @cart1.paid_total, 162_500
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# wallet
assert_equal 0, @admin.wallet.amount
assert_equal 2, @admin.wallet.wallet_transactions.count
transaction = @admin.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal @cart1.wallet_amount / 100.0, transaction.amount
assert_equal @cart1.wallet_transaction_id, transaction.id
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'admin pay user order by local with success' do
login_as(@admin, scope: :user)
@ -254,63 +131,6 @@ class Store::AdminPayOrderTest < ActionDispatch::IntegrationTest
assert_equal activity.operator_profile_id, @admin.invoicing_profile.id
end
test 'admin pay himself order by wallet with success' do
login_as(@admin, scope: :user)
service = WalletService.new(user: @admin, wallet: @admin.wallet)
service.credit(@cart1.total / 100)
invoice_count = Invoice.count
invoice_items_count = InvoiceItem.count
users_credit_count = UsersCredit.count
wallet_transactions_count = WalletTransaction.count
post '/api/checkout/payment',
params: {
order_token: @cart1.token,
customer_id: @admin.id
}.to_json, headers: default_headers
@admin.wallet.reload
@cart1.reload
# general assertions
assert_equal 200, response.status
assert_equal @cart1.state, 'paid'
assert_equal invoice_count + 1, Invoice.count
assert_equal invoice_items_count + 2, InvoiceItem.count
assert_equal users_credit_count, UsersCredit.count
assert_equal wallet_transactions_count + 1, WalletTransaction.count
# invoice_items assertions
invoice_item = InvoiceItem.last
assert invoice_item.check_footprint
# invoice assertions
invoice = Invoice.last
assert_invoice_pdf invoice
assert_not_nil invoice.debug_footprint
assert invoice.payment_gateway_object.blank?
assert_not invoice.total.blank?
assert invoice.check_footprint
# notification
assert_not_empty Notification.where(attached_object: invoice)
# wallet
assert_equal 0, @admin.wallet.amount
assert_equal 2, @admin.wallet.wallet_transactions.count
transaction = @admin.wallet.wallet_transactions.last
assert_equal 'debit', transaction.transaction_type
assert_equal @cart1.paid_total, 0
assert_equal @cart1.wallet_amount / 100.0, transaction.amount
assert_equal @cart1.payment_method, 'wallet'
assert_equal @cart1.wallet_transaction_id, transaction.id
assert_equal invoice.wallet_amount / 100.0, transaction.amount
end
test 'admin pay user order by wallet with success' do
login_as(@admin, scope: :user)

View File

@ -91,6 +91,10 @@ class ReservationSubscriptionStatisticServiceTest < ActionDispatch::IntegrationT
]
}.to_json, headers: default_headers
Stats::Machine.refresh_index!
Stats::Training.refresh_index!
Stats::Subscription.refresh_index!
# Build the stats for the last 3 days, we expect the above invoices (reservations+subscription) to appear in the resulting stats
::Statistics::BuilderService.generate_statistic({ start_date: 2.days.ago.beginning_of_day,
end_date: DateTime.current.end_of_day })
@ -126,6 +130,8 @@ class ReservationSubscriptionStatisticServiceTest < ActionDispatch::IntegrationT
check_statistics_on_user(stat_hour)
# training
Stats::Training.refresh_index!
stat_training = Stats::Training.search(query: { bool: { must: [{ term: { date: 1.day.ago.to_date.iso8601 } },
{ term: { type: 'booking' } }] } }).first
assert_not_nil stat_training

View File

@ -0,0 +1,338 @@
---
http_interactions:
- request:
method: post
uri: https://api.stripe.com/v1/payment_methods
body:
encoding: UTF-8
string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2023&card[cvc]=314
headers:
User-Agent:
- Stripe/v1 RubyBindings/5.29.0
Authorization:
- Bearer sk_test_testfaketestfaketestfake
Content-Type:
- application/x-www-form-urlencoded
Stripe-Version:
- '2019-08-14'
X-Stripe-Client-User-Agent:
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
MacBook-Pro-Sleede-Peng 20.6.0 Darwin Kernel Version 20.6.0: Thu Sep 29 20:15:11
PDT 2022; root:xnu-7195.141.42~1/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Mon, 07 Nov 2022 17:22:23 GMT
Content-Type:
- application/json
Content-Length:
- '930'
Connection:
- keep-alive
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, POST, HEAD, OPTIONS, DELETE
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Idempotency-Key:
- 3d420ca2-b79a-451b-88bc-56efb989ae3e
Original-Request:
- req_7tnZKRYjblvjJF
Request-Id:
- req_7tnZKRYjblvjJF
Stripe-Should-Retry:
- 'false'
Stripe-Version:
- '2019-08-14'
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"id": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"object": "payment_method",
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "unchecked"
},
"country": "US",
"exp_month": 4,
"exp_year": 2023,
"fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit",
"generated_from": null,
"last4": "4242",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": null
},
"created": 1667841743,
"customer": null,
"livemode": false,
"metadata": {},
"type": "card"
}
recorded_at: Mon, 07 Nov 2022 17:22:24 GMT
- request:
method: post
uri: https://api.stripe.com/v1/payment_intents
body:
encoding: UTF-8
string: payment_method=pm_1M1Z1f2sOmf47Nz9y4qaYQap&amount=162500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8CyNk3UTi8lvCc
headers:
User-Agent:
- Stripe/v1 RubyBindings/5.29.0
Authorization:
- Bearer sk_test_testfaketestfaketestfake
Content-Type:
- application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_7tnZKRYjblvjJF","request_duration_ms":697}}'
Stripe-Version:
- '2019-08-14'
X-Stripe-Client-User-Agent:
- '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin
MacBook-Pro-Sleede-Peng 20.6.0 Darwin Kernel Version 20.6.0: Thu Sep 29 20:15:11
PDT 2022; root:xnu-7195.141.42~1/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}'
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Mon, 07 Nov 2022 17:22:25 GMT
Content-Type:
- application/json
Content-Length:
- '4476'
Connection:
- keep-alive
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Methods:
- GET, POST, HEAD, OPTIONS, DELETE
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required
Access-Control-Max-Age:
- '300'
Cache-Control:
- no-cache, no-store
Idempotency-Key:
- 5294d6d7-d766-4aa5-bac1-be52f491fa20
Original-Request:
- req_c0S6XFCR5hlcc9
Request-Id:
- req_c0S6XFCR5hlcc9
Stripe-Should-Retry:
- 'false'
Stripe-Version:
- '2019-08-14'
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"id": "pi_3M1Z1g2sOmf47Nz91KcAbrWR",
"object": "payment_intent",
"amount": 162500,
"amount_capturable": 0,
"amount_details": {
"tip": {}
},
"amount_received": 162500,
"application": null,
"application_fee_amount": null,
"automatic_payment_methods": null,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"charges": {
"object": "list",
"data": [
{
"id": "ch_3M1Z1g2sOmf47Nz91YJQvMPK",
"object": "charge",
"amount": 162500,
"amount_captured": 162500,
"amount_refunded": 0,
"application": null,
"application_fee": null,
"application_fee_amount": null,
"balance_transaction": "txn_3M1Z1g2sOmf47Nz913ZvXNvF",
"billing_details": {
"address": {
"city": null,
"country": null,
"line1": null,
"line2": null,
"postal_code": null,
"state": null
},
"email": null,
"name": null,
"phone": null
},
"calculated_statement_descriptor": "Stripe",
"captured": true,
"created": 1667841744,
"currency": "usd",
"customer": "cus_8CyNk3UTi8lvCc",
"description": null,
"destination": null,
"dispute": null,
"disputed": false,
"failure_balance_transaction": null,
"failure_code": null,
"failure_message": null,
"fraud_details": {},
"invoice": null,
"livemode": false,
"metadata": {},
"on_behalf_of": null,
"order": null,
"outcome": {
"network_status": "approved_by_network",
"reason": null,
"risk_level": "normal",
"risk_score": 45,
"seller_message": "Payment complete.",
"type": "authorized"
},
"paid": true,
"payment_intent": "pi_3M1Z1g2sOmf47Nz91KcAbrWR",
"payment_method": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"payment_method_details": {
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": "pass"
},
"country": "US",
"exp_month": 4,
"exp_year": 2023,
"fingerprint": "o52jybR7bnmNn6AT",
"funding": "credit",
"installments": null,
"last4": "4242",
"mandate": null,
"network": "visa",
"three_d_secure": null,
"wallet": null
},
"type": "card"
},
"receipt_email": null,
"receipt_number": null,
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNH9pJsGMgbMAdBS9Kg6LBbMQFUWP1mmaiHjAUCgW-WYHRH5dwIHTlYhiTVjiSL5fqEMQr17GSJhWPA-",
"refunded": false,
"refunds": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/charges/ch_3M1Z1g2sOmf47Nz91YJQvMPK/refunds"
},
"review": null,
"shipping": null,
"source": null,
"source_transfer": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "succeeded",
"transfer_data": null,
"transfer_group": null
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/charges?payment_intent=pi_3M1Z1g2sOmf47Nz91KcAbrWR"
},
"client_secret": "pi_3M1Z1g2sOmf47Nz91KcAbrWR_secret_ocSaI8LfCIvNBzfXl5iwRB9kS",
"confirmation_method": "manual",
"created": 1667841744,
"currency": "usd",
"customer": "cus_8CyNk3UTi8lvCc",
"description": null,
"invoice": null,
"last_payment_error": null,
"livemode": false,
"metadata": {},
"next_action": null,
"on_behalf_of": null,
"payment_method": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"payment_method_options": {
"card": {
"installments": null,
"mandate_options": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"processing": null,
"receipt_email": null,
"review": null,
"setup_future_usage": null,
"shipping": null,
"source": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "succeeded",
"transfer_data": null,
"transfer_group": null
}
recorded_at: Mon, 07 Nov 2022 17:22:26 GMT
recorded_with: VCR 6.0.0

View File

@ -13,6 +13,8 @@ http_interactions:
- Bearer sk_test_testfaketestfaketestfake
Content-Type:
- application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_Xmih0ndHQjzde4","request_duration_ms":2}}'
Stripe-Version:
- '2019-08-14'
X-Stripe-Client-User-Agent:
@ -31,7 +33,7 @@ http_interactions:
Server:
- nginx
Date:
- Mon, 07 Nov 2022 17:22:23 GMT
- Tue, 22 Nov 2022 10:23:29 GMT
Content-Type:
- application/json
Content-Length:
@ -51,11 +53,11 @@ http_interactions:
Cache-Control:
- no-cache, no-store
Idempotency-Key:
- 3d420ca2-b79a-451b-88bc-56efb989ae3e
- 524422f2-06be-4b13-a164-1d78f9221db3
Original-Request:
- req_7tnZKRYjblvjJF
- req_LJ3F130BMDTCDc
Request-Id:
- req_7tnZKRYjblvjJF
- req_LJ3F130BMDTCDc
Stripe-Should-Retry:
- 'false'
Stripe-Version:
@ -66,7 +68,7 @@ http_interactions:
encoding: UTF-8
string: |-
{
"id": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"id": "pm_1M6tdV2sOmf47Nz9ZhwXBRTL",
"object": "payment_method",
"billing_details": {
"address": {
@ -106,19 +108,19 @@ http_interactions:
},
"wallet": null
},
"created": 1667841743,
"created": 1669112609,
"customer": null,
"livemode": false,
"metadata": {},
"type": "card"
}
recorded_at: Mon, 07 Nov 2022 17:22:24 GMT
recorded_at: Tue, 22 Nov 2022 10:23:29 GMT
- request:
method: post
uri: https://api.stripe.com/v1/payment_intents
body:
encoding: UTF-8
string: payment_method=pm_1M1Z1f2sOmf47Nz9y4qaYQap&amount=162500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8CyNk3UTi8lvCc
string: payment_method=pm_1M6tdV2sOmf47Nz9ZhwXBRTL&amount=162500&currency=usd&confirmation_method=manual&confirm=true&customer=cus_8CyNk3UTi8lvCc
headers:
User-Agent:
- Stripe/v1 RubyBindings/5.29.0
@ -127,7 +129,7 @@ http_interactions:
Content-Type:
- application/x-www-form-urlencoded
X-Stripe-Client-Telemetry:
- '{"last_request_metrics":{"request_id":"req_7tnZKRYjblvjJF","request_duration_ms":697}}'
- '{"last_request_metrics":{"request_id":"req_LJ3F130BMDTCDc","request_duration_ms":737}}'
Stripe-Version:
- '2019-08-14'
X-Stripe-Client-User-Agent:
@ -146,7 +148,7 @@ http_interactions:
Server:
- nginx
Date:
- Mon, 07 Nov 2022 17:22:25 GMT
- Tue, 22 Nov 2022 10:23:31 GMT
Content-Type:
- application/json
Content-Length:
@ -166,11 +168,11 @@ http_interactions:
Cache-Control:
- no-cache, no-store
Idempotency-Key:
- 5294d6d7-d766-4aa5-bac1-be52f491fa20
- 3c835ee8-86ea-4896-811f-0d3e0785cd09
Original-Request:
- req_c0S6XFCR5hlcc9
- req_ybBnHONjaSyx1X
Request-Id:
- req_c0S6XFCR5hlcc9
- req_ybBnHONjaSyx1X
Stripe-Should-Retry:
- 'false'
Stripe-Version:
@ -181,7 +183,7 @@ http_interactions:
encoding: UTF-8
string: |-
{
"id": "pi_3M1Z1g2sOmf47Nz91KcAbrWR",
"id": "pi_3M6tdW2sOmf47Nz90KZ43vXZ",
"object": "payment_intent",
"amount": 162500,
"amount_capturable": 0,
@ -199,7 +201,7 @@ http_interactions:
"object": "list",
"data": [
{
"id": "ch_3M1Z1g2sOmf47Nz91YJQvMPK",
"id": "ch_3M6tdW2sOmf47Nz90CWztIp1",
"object": "charge",
"amount": 162500,
"amount_captured": 162500,
@ -207,7 +209,7 @@ http_interactions:
"application": null,
"application_fee": null,
"application_fee_amount": null,
"balance_transaction": "txn_3M1Z1g2sOmf47Nz913ZvXNvF",
"balance_transaction": "txn_3M6tdW2sOmf47Nz90xHPB7ge",
"billing_details": {
"address": {
"city": null,
@ -223,7 +225,7 @@ http_interactions:
},
"calculated_statement_descriptor": "Stripe",
"captured": true,
"created": 1667841744,
"created": 1669112610,
"currency": "usd",
"customer": "cus_8CyNk3UTi8lvCc",
"description": null,
@ -243,13 +245,13 @@ http_interactions:
"network_status": "approved_by_network",
"reason": null,
"risk_level": "normal",
"risk_score": 45,
"risk_score": 19,
"seller_message": "Payment complete.",
"type": "authorized"
},
"paid": true,
"payment_intent": "pi_3M1Z1g2sOmf47Nz91KcAbrWR",
"payment_method": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"payment_intent": "pi_3M6tdW2sOmf47Nz90KZ43vXZ",
"payment_method": "pm_1M6tdV2sOmf47Nz9ZhwXBRTL",
"payment_method_details": {
"card": {
"brand": "visa",
@ -274,14 +276,14 @@ http_interactions:
},
"receipt_email": null,
"receipt_number": null,
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KNH9pJsGMgbMAdBS9Kg6LBbMQFUWP1mmaiHjAUCgW-WYHRH5dwIHTlYhiTVjiSL5fqEMQr17GSJhWPA-",
"receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xMDNyRTYyc09tZjQ3Tno5KKPG8psGMgbPi1sBzwo6LBYq6qekkFXHe51z6Ei_GniUXrfPnNwAP9pxTHhMG3SSyEbSD-_jgxqNwc0o",
"refunded": false,
"refunds": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/charges/ch_3M1Z1g2sOmf47Nz91YJQvMPK/refunds"
"url": "/v1/charges/ch_3M6tdW2sOmf47Nz90CWztIp1/refunds"
},
"review": null,
"shipping": null,
@ -296,11 +298,11 @@ http_interactions:
],
"has_more": false,
"total_count": 1,
"url": "/v1/charges?payment_intent=pi_3M1Z1g2sOmf47Nz91KcAbrWR"
"url": "/v1/charges?payment_intent=pi_3M6tdW2sOmf47Nz90KZ43vXZ"
},
"client_secret": "pi_3M1Z1g2sOmf47Nz91KcAbrWR_secret_ocSaI8LfCIvNBzfXl5iwRB9kS",
"client_secret": "pi_3M6tdW2sOmf47Nz90KZ43vXZ_secret_EZPT1VWG8jNZgqaW0iqXN0LjY",
"confirmation_method": "manual",
"created": 1667841744,
"created": 1669112610,
"currency": "usd",
"customer": "cus_8CyNk3UTi8lvCc",
"description": null,
@ -310,7 +312,7 @@ http_interactions:
"metadata": {},
"next_action": null,
"on_behalf_of": null,
"payment_method": "pm_1M1Z1f2sOmf47Nz9y4qaYQap",
"payment_method": "pm_1M6tdV2sOmf47Nz9ZhwXBRTL",
"payment_method_options": {
"card": {
"installments": null,
@ -334,5 +336,5 @@ http_interactions:
"transfer_data": null,
"transfer_group": null
}
recorded_at: Mon, 07 Nov 2022 17:22:26 GMT
recorded_at: Tue, 22 Nov 2022 10:23:31 GMT
recorded_with: VCR 6.0.0