1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

move the architecture from stripe-only to gateway-generic

This commit is contained in:
Sylvain 2021-04-15 17:01:52 +02:00
parent ddd1ac52d6
commit 1bbb8c3965
13 changed files with 239 additions and 68 deletions

View File

@ -66,7 +66,7 @@ class API::PayzenController < API::PaymentsController
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation,
payment_details: details,
intent_id: order_id, # TODO: change to gateway_id
payment_id: order_id,
schedule: params[:cart_items][:reservation][:payment_schedule],
payment_method: payment_method)
if is_reserve

View File

@ -117,7 +117,7 @@ class API::StripeController < API::PaymentsController
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation,
payment_details: details,
intent_id: intent.id,
payment_id: intent.id,
schedule: params[:cart_items][:reservation][:payment_schedule],
payment_method: payment_method)
if intent.class == Stripe::PaymentIntent
@ -147,7 +147,7 @@ class API::StripeController < API::PaymentsController
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
.pay_and_save(@subscription,
payment_details: details,
intent_id: intent.id,
payment_id: intent.id,
schedule: params[:cart_items][:subscription][:payment_schedule],
payment_method: 'stripe')
if intent.class == Stripe::PaymentIntent

View File

@ -73,8 +73,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
payment_schedule: undefined // the effective computed payment schedule
};
// online payments (stripe)
$scope.stripe = {
// online payments (by card)
$scope.onlinePayment = {
showModal: false,
cartItems: undefined
};
@ -313,11 +313,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
};
/**
* This will open/close the stripe payment modal
* This will open/close the online payment modal
*/
$scope.toggleStripeModal = (beforeApply) => {
$scope.toggleOnlinePaymentModal = (beforeApply) => {
setTimeout(() => {
$scope.stripe.showModal = !$scope.stripe.showModal;
$scope.onlinePayment.showModal = !$scope.onlinePayment.showModal;
if (typeof beforeApply === 'function') {
beforeApply();
}
@ -326,11 +326,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
};
/**
* Invoked atfer a successful Stripe payment
* Invoked atfer a successful card payment
* @param result {*} may be a reservation or a subscription
*/
$scope.afterStripeSuccess = (result) => {
$scope.toggleStripeModal();
$scope.afterOnlinePaymentSuccess = (result) => {
$scope.toggleOnlinePaymentModal();
afterPayment(result);
};
@ -682,7 +682,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
* @param planId {number}
* @param userId {number}
* @param schedule {boolean}
* @param method {String} 'stripe' | ''
* @param method {String} 'stripe' | 'payzen' | ''
* @return {{subscription: {payment_schedule: boolean, user_id: number, plan_id: number}}}
*/
const mkSubscription = function (planId, userId, schedule, method) {
@ -715,13 +715,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
/**
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
*/
const payByStripe = function (reservation) {
const payOnline = function (reservation) {
// check that the online payment is enabled
if ($scope.settings.online_payment_module !== 'true') {
growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
$scope.toggleStripeModal(() => {
$scope.stripe.cartItems = mkCartItems(reservation, 'stripe');
$scope.toggleOnlinePaymentModal(() => {
$scope.onlinePayment.cartItems = mkCartItems(reservation, $scope.settings.payment_gateway);
});
}
};
@ -740,7 +740,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
},
cartItems () {
return mkCartItems(reservation, 'stripe');
return mkCartItems(reservation, $scope.settings.payment_gateway);
},
wallet () {
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
@ -787,15 +787,15 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
// how should we collect payments for the payment schedule
$scope.method = {
payment_method: 'stripe'
payment_method: settings.payment_gateway
};
// "valid" Button label
$scope.validButtonName = '';
// stripe modal state
// online payment modal state
// this is used to collect card data when a payment-schedule was selected, and paid with a card
$scope.isOpenStripeModal = false;
$scope.isOpenOnlinePaymentModal = false;
// the customer
$scope.user = user;
@ -804,12 +804,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
* Callback to process the local payment, triggered on button click
*/
$scope.ok = function () {
if ($scope.schedule && $scope.method.payment_method === 'stripe') {
if ($scope.schedule && $scope.method.payment_method === settings.payment_gateway) {
// check that the online payment is enabled
if (settings.online_payment_module !== 'true') {
return growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return $scope.toggleStripeModal();
return $scope.toggleOnlinePaymentModal();
}
}
$scope.attempting = true;
@ -844,11 +844,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
/**
* Asynchronously updates the status of the stripe modal
* Asynchronously updates the status of the online payment modal
*/
$scope.toggleStripeModal = function () {
$scope.toggleOnlinePaymentModal = function () {
setTimeout(() => {
$scope.isOpenStripeModal = !$scope.isOpenStripeModal;
$scope.isOpenOnlinePaymentModal = !$scope.isOpenOnlinePaymentModal;
$scope.$apply();
}, 50);
};
@ -858,7 +858,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
* @param result {*} Reservation or Subscription
*/
$scope.afterCreatePaymentSchedule = function (result) {
$scope.toggleStripeModal();
$scope.toggleOnlinePaymentModal();
$uibModalInstance.close(result);
};
@ -883,7 +883,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
if (AuthService.isAuthorized(['admin', 'manager']) && $rootScope.currentUser.id !== reservation.user_id) {
method = $scope.method.payment_method;
} else {
method = 'stripe';
method = settings.payment_gateway;
}
}
if ($scope.amount > 0) {
@ -931,7 +931,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
};
/**
* Actions to pay slots
* Actions to pay slots (or subscription)
*/
const paySlots = function () {
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
@ -940,7 +940,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if ((AuthService.isAuthorized(['member']) && (amountToPay > 0 || (amountToPay === 0 && hasOtherDeadlines()))) ||
(AuthService.isAuthorized('manager') && $scope.user.id === $rootScope.currentUser.id && amountToPay > 0)) {
return payByStripe(reservation);
return payOnline(reservation);
} else {
if (AuthService.isAuthorized(['admin']) ||
(AuthService.isAuthorized('manager') && $scope.user.id !== $rootScope.currentUser.id) ||

View File

@ -363,7 +363,7 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
names: "['machine_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', " +
"'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'online_payment_module']"
"'online_payment_module', 'payment_gateway']"
}).$promise;
}]
}
@ -449,7 +449,7 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'space_explications_alert', 'online_payment_module']"
"'space_explications_alert', 'online_payment_module', 'payment_gateway']"
}).$promise;
}]
}
@ -502,7 +502,8 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
"'training_explications_alert', 'training_information_message', 'online_payment_module']"
"'training_explications_alert', 'training_information_message', 'online_payment_module', " +
"'payment_gateway']"
}).$promise;
}]
}
@ -532,7 +533,7 @@ angular.module('application.router', ['ui.router'])
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway']" }).$promise; }]
}
})

View File

@ -84,7 +84,7 @@
<div ng-hide="cookiesStatus" translate>{{ 'app.logged.dashboard.settings.cookies_unset' }}</div>
<button ng-click="resetCookies()" ng-show="cookiesStatus" class="btn text-black btn-warning-full btn-sm m-t-xs" translate>{{ 'app.logged.dashboard.settings.reset_cookies' }}</button>
</div>
<div class="widget-content no-bg text-center auto wrapper" ng-hide="isAdminSysgit add ">
<div class="widget-content no-bg text-center auto wrapper" ng-hide="isAdminSys">
<button class="btn text-white btn-danger btn-sm" ng-click="deleteUser(user)"><i class="fa fa-warning m-r-xs"></i> {{ 'app.logged.dashboard.settings.delete_my_account' | translate }}</button>
</div>
</div>

View File

@ -199,11 +199,11 @@
</div>
</div>
<div ng-if="stripe.showModal">
<payment-modal is-open="stripe.showModal"
toggle-modal="toggleStripeModal"
after-success="afterStripeSuccess"
cart-items="stripe.cartItems"
<div ng-if="onlinePayment.showModal">
<payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
cart-items="onlinePayment.cartItems"
current-user="currentUser"
customer="user"
schedule="schedule.payment_schedule"/>

View File

@ -66,19 +66,24 @@ class InvoicesService
# @param operator_profile_id {Number} ID of the user that operates the invoice generation (may be an admin, a manager or the customer himself)
# @param reservation {Reservation} the booking reservation, if any
# @param subscription {Subscription} the booking subscription, if any
# @param payment_intent_id {String} ID of the Stripe::PaymentIntend, if the current invoice is paid by stripe
# @param payment_id {String} ID of the payment, a returned by the gateway, if the current invoice is paid by card
# @param payment_method {String} the payment method used
##
def self.create(payment_details, operator_profile_id, reservation: nil, subscription: nil, payment_intent_id: nil)
def self.create(payment_details, operator_profile_id, reservation: nil, subscription: nil, payment_id: nil, payment_method: nil)
user = reservation&.user || subscription&.user
operator = InvoicingProfile.find(operator_profile_id)&.user
method = operator&.admin? || (operator&.manager? && operator != user) ? nil : 'stripe'
method = if payment_method
payment_method
else
operator&.admin? || (operator&.manager? && operator != user) ? nil : Setting.get('payment_gateway')
end
invoice = Invoice.new(
invoiced: subscription || reservation,
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id,
stp_payment_intent_id: payment_intent_id,
stp_payment_intent_id: payment_id,
payment_method: method
)

View File

@ -49,7 +49,7 @@ class PaymentScheduleService
{ payment_schedule: ps, items: items }
end
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil, setup_intent_id: nil)
def create(subscription, total, coupon: nil, operator: nil, payment_method: nil, reservation: nil, user: nil, payment_id: nil)
subscription = reservation.generate_subscription if !subscription && reservation&.plan_id
raise InvalidSubscriptionError unless subscription&.persisted?
@ -59,7 +59,7 @@ class PaymentScheduleService
ps.scheduled = reservation || subscription
ps.payment_method = payment_method
ps.stp_setup_intent_id = setup_intent_id
ps.stp_setup_intent_id = payment_id
ps.operator_profile = operator.invoicing_profile
ps.invoicing_profile = user.invoicing_profile
ps.statistic_profile = user.statistic_profile
@ -72,17 +72,18 @@ class PaymentScheduleService
##
# Generate the invoice associated with the given PaymentScheduleItem, with the children elements (InvoiceItems).
# @param stp_invoice is used to determine if the invoice was paid using stripe
# @param payment_method {String} the payment method or gateway in use
# @param payment_id {String} the identifier of the payment as provided by the payment gateway, in case of card payment
##
def generate_invoice(payment_schedule_item, stp_invoice = nil)
def generate_invoice(payment_schedule_item, payment_method: nil, payment_id: nil)
# build the base invoice
invoice = Invoice.new(
invoiced: payment_schedule_item.payment_schedule.scheduled,
invoicing_profile: payment_schedule_item.payment_schedule.invoicing_profile,
statistic_profile: payment_schedule_item.payment_schedule.statistic_profile,
operator_profile_id: payment_schedule_item.payment_schedule.operator_profile_id,
stp_payment_intent_id: stp_invoice&.payment_intent,
payment_method: stp_invoice ? 'stripe' : nil
stp_payment_intent_id: payment_id,
payment_method: payment_method
)
# complete the invoice with some InvoiceItem
if payment_schedule_item.first?

View File

@ -10,10 +10,10 @@ class Reservations::Reserve
end
##
# Confirm the payment of the given reservation, generate the associated documents and save teh record into
# Confirm the payment of the given reservation, generate the associated documents and save the record into
# the database.
##
def pay_and_save(reservation, payment_details: nil, intent_id: nil, schedule: false, payment_method: nil)
def pay_and_save(reservation, payment_details: nil, payment_id: nil, schedule: false, payment_method: nil)
user = User.find(user_id)
reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id
@ -26,15 +26,19 @@ class Reservations::Reserve
user: user,
payment_method: payment_method,
coupon: payment_details[:coupon],
setup_intent_id: intent_id)
payment_id: payment_id)
else
generate_invoice(reservation, operator_profile_id, payment_details, intent_id)
generate_invoice(reservation,
operator_profile_id,
payment_details,
payment_id: payment_id,
payment_method: payment_method)
end
WalletService.debit_user_wallet(payment, user, reservation)
reservation.save
reservation.post_save
payment.save
payment.post_save(intent_id)
payment.post_save(payment_id)
end
true
end
@ -45,7 +49,7 @@ class Reservations::Reserve
# Generate the invoice for the given reservation+subscription
##
def generate_schedule(reservation: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon: nil,
setup_intent_id: nil)
payment_id: nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
PaymentScheduleService.new.create(
@ -56,19 +60,20 @@ class Reservations::Reserve
payment_method: payment_method,
user: user,
reservation: reservation,
setup_intent_id: setup_intent_id
payment_id: payment_id
)
end
##
# Generate the invoice for the given reservation
##
def generate_invoice(reservation, operator_profile_id, payment_details, payment_intent_id = nil)
def generate_invoice(reservation, operator_profile_id, payment_details, payment_id: nil, payment_method: nil)
InvoicesService.create(
payment_details,
operator_profile_id,
reservation: reservation,
payment_intent_id: payment_intent_id
payment_id: payment_id,
payment_method: payment_method
)
end

View File

@ -12,11 +12,11 @@ class Subscriptions::Subscribe
##
# @param subscription {Subscription}
# @param payment_details {Hash} as generated by Price.compute
# @param intent_id {String} from stripe
# @param payment_id {String} from the payment gateway
# @param schedule {Boolean}
# @param payment_method {String} only for schedules
##
def pay_and_save(subscription, payment_details: nil, intent_id: nil, schedule: false, payment_method: nil)
def pay_and_save(subscription, payment_details: nil, payment_id: nil, schedule: false, payment_method: nil)
return false if user_id.nil?
user = User.find(user_id)
@ -33,13 +33,17 @@ class Subscriptions::Subscribe
user: user,
payment_method: payment_method,
coupon: payment_details[:coupon],
setup_intent_id: intent_id)
payment_id: payment_id)
else
generate_invoice(subscription, operator_profile_id, payment_details, intent_id)
generate_invoice(subscription,
operator_profile_id,
payment_details,
payment_id: payment_id,
payment_method: payment_method)
end
WalletService.debit_user_wallet(payment, user, subscription)
payment.save
payment.post_save(intent_id)
payment.post_save(payment_id)
end
true
end
@ -61,7 +65,7 @@ class Subscriptions::Subscribe
operator_profile_id: operator_profile_id,
user: new_sub.user,
payment_method: schedule.payment_method,
setup_intent_id: schedule.stp_setup_intent_id)
payment_id: schedule.stp_setup_intent_id)
else
generate_invoice(subscription, operator_profile_id, details)
end
@ -79,7 +83,7 @@ class Subscriptions::Subscribe
# Generate the invoice for the given subscription
##
def generate_schedule(subscription: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon: nil,
setup_intent_id: nil)
payment_id: nil)
operator = InvoicingProfile.find(operator_profile_id)&.user
PaymentScheduleService.new.create(
@ -89,19 +93,20 @@ class Subscriptions::Subscribe
operator: operator,
payment_method: payment_method,
user: user,
setup_intent_id: setup_intent_id
payment_id: payment_id
)
end
##
# Generate the invoice for the given subscription
##
def generate_invoice(subscription, operator_profile_id, payment_details, payment_intent_id = nil)
def generate_invoice(subscription, operator_profile_id, payment_details, payment_id: nil, payment_method: nil)
InvoicesService.create(
payment_details,
operator_profile_id,
subscription: subscription,
payment_intent_id: payment_intent_id
payment_id: payment_id,
payment_method: payment_method
)
end

View File

@ -25,7 +25,7 @@ class PaymentScheduleItemWorker
stp_invoice = Stripe::Invoice.retrieve(stp_subscription.latest_invoice, api_key: stripe_key)
if stp_invoice.status == 'paid'
##### Stripe / Successfully paid
PaymentScheduleService.new.generate_invoice(psi, stp_invoice)
PaymentScheduleService.new.generate_invoice(psi, payment_method: 'stripe', payment_id: stp_invoice.payment_intent)
psi.update_attributes(state: 'paid', payment_method: 'stripe', stp_invoice_id: stp_invoice.id)
elsif stp_subscription.status == 'past_due' || stp_invoice.status == 'open'
##### Stripe / Payment error

View File

@ -696,6 +696,94 @@ history_value_73:
value_history_74:
id: 74
setting_id: 74
invoicing_profile_id: 1
value: until_start
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_75:
id: 75
setting_id: 75
invoicing_profile_id: 1
value: FabManager_paymentSchedule
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_76:
id: 76
setting_id: 76
invoicing_profile_id: 1
value: true
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_77:
id: 77
setting_id: 77
invoicing_profile_id: 1
value: false
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_78:
id: 78
setting_id: 78
invoicing_profile_id: 1
value: stripe
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_79:
id: 79
setting_id: 79
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_80:
id: 80
setting_id: 80
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_81:
id: 81
setting_id: 81
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_82:
id: 82
setting_id: 82
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_83:
id: 83
setting_id: 83
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_84:
id: 84
setting_id: 84
invoicing_profile_id: 1
value:
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
value_history_85:
id: 85
setting_id: 10
invoicing_profile_id: 1
value: YYMMmmmX[/VL]R[/A]S[/E]

View File

@ -431,3 +431,69 @@ setting_73:
created_at: 2020-06-17 10:48:19.002417000 Z
updated_at: 2020-06-17 10:48:19.002417000 Z
setting_74:
id: 74
name: upcoming_events_shown
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_75:
id: 75
name: payment_schedule_prefix
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_76:
id: 76
name: trainings_module
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_77:
id: 77
name: address_required
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_78:
id: 78
name: payment_gateway
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_79:
id: 79
name: payzen_username
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_80:
id: 80
name: payzen_password
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_81:
id: 81
name: payzen_endpoint
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_82:
id: 82
name: payzen_public_key
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_83:
id: 83
name: payzen_hmac
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z
setting_84:
id: 84
name: payzen_currency
created_at: 2020-04-15 14:38:40.000421500 Z
updated_at: 2020-04-15 14:38:40.000421500 Z