spree_cm_commissioner 2.13.4 → 2.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env.example +5 -0
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/base_controller_decorator.rb +14 -0
- data/app/controllers/spree/admin/customer_notifications_controller.rb +16 -1
- data/app/controllers/spree/admin/inventory_items_controller.rb +11 -0
- data/app/controllers/spree/admin/payment_fee_rules_controller.rb +76 -0
- data/app/controllers/spree/admin/prepaid_transactions_controller.rb +69 -0
- data/app/controllers/spree/admin/private_sales_controller.rb +6 -0
- data/app/controllers/spree/admin/product_commissions_controller.rb +2 -1
- data/app/controllers/spree/admin/reschedule_policies_controller.rb +148 -0
- data/app/controllers/spree/admin/reschedule_requests_controller.rb +82 -0
- data/app/controllers/spree/admin/service_fee_rules_controller.rb +1 -0
- data/app/controllers/spree/admin/store_credit_admin_controller.rb +122 -0
- data/app/controllers/spree/admin/ticket_transfer_rules_controller.rb +7 -0
- data/app/controllers/spree/api/v2/platform/store_credits_controller_decorator.rb +35 -0
- data/app/controllers/spree/api/v2/storefront/account/prepaid_balances_controller.rb +52 -0
- data/app/controllers/spree/api/v2/storefront/account/prepaid_transactions_controller.rb +56 -0
- data/app/controllers/spree/api/v2/storefront/cart_payment_fees_controller.rb +48 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +37 -0
- data/app/controllers/spree/api/v2/storefront/prepaid_config_controller.rb +22 -0
- data/app/controllers/spree/api/v2/storefront/reschedule_requests_controller.rb +156 -0
- data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +3 -1
- data/app/controllers/spree/api/v2/storefront/transit/item_availabilities_controller.rb +66 -0
- data/app/controllers/spree/api/v2/tenant/account/prepaid_balances_controller.rb +51 -0
- data/app/controllers/spree/api/v2/tenant/account/prepaid_transactions_controller.rb +53 -0
- data/app/controllers/spree/api/v2/tenant/base_controller.rb +20 -0
- data/app/controllers/spree/api/v2/tenant/cart_payment_fees_controller.rb +43 -0
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +42 -0
- data/app/controllers/spree/api/v2/tenant/prepaid_config_controller.rb +29 -0
- data/app/controllers/spree_cm_commissioner/admin/store_credits_controller_decorator.rb +217 -0
- data/app/errors/spree_cm_commissioner/reschedule_policies/invalid_policyable_error.rb +8 -0
- data/app/errors/spree_cm_commissioner/reschedule_policies/not_reschedulable_error.rb +11 -0
- data/app/errors/spree_cm_commissioner/reschedule_requests/cancel_not_allowed_error.rb +6 -0
- data/app/errors/spree_cm_commissioner/reschedule_requests/complete_cash_payment_error.rb +6 -0
- data/app/errors/spree_cm_commissioner/reschedule_requests/eligibility_denied_error.rb +7 -0
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +6 -2
- data/app/factory/spree_cm_commissioner/reschedule_telegram_message_factory.rb +141 -0
- data/app/finders/spree_cm_commissioner/inventory_items/recently_changed_finder.rb +1 -1
- data/app/helpers/spree/admin/base_helper_decorator.rb +12 -0
- data/app/helpers/spree/admin/reschedule_policies_helper.rb +35 -0
- data/app/helpers/spree/admin/reschedule_requests_helper.rb +79 -0
- data/app/helpers/spree_cm_commissioner/admin/fee_rules_helper.rb +23 -0
- data/app/interactors/spree_cm_commissioner/create_ticket.rb +1 -0
- data/app/jobs/spree_cm_commissioner/audit_event_job.rb +29 -0
- data/app/jobs/spree_cm_commissioner/prepaid_reconciliation_cron_job.rb +9 -0
- data/app/jobs/spree_cm_commissioner/prepaid_reverse_top_up_job.rb +16 -0
- data/app/jobs/spree_cm_commissioner/prepaid_setup_types_job.rb +24 -0
- data/app/jobs/spree_cm_commissioner/prepaid_top_up_credit_job.rb +31 -0
- data/app/jobs/spree_cm_commissioner/reschedule_requests/approval_reminder_job.rb +17 -0
- data/app/jobs/spree_cm_commissioner/reschedule_requests/execute_job.rb +20 -0
- data/app/jobs/spree_cm_commissioner/reschedule_requests/expire_overdue_requests_job.rb +12 -0
- data/app/jobs/spree_cm_commissioner/reschedule_requests/expire_request_job.rb +15 -0
- data/app/jobs/spree_cm_commissioner/reschedule_requests/payment_reminder_job.rb +17 -0
- data/app/models/concerns/spree_cm_commissioner/audit_wrapper.rb +139 -0
- data/app/models/concerns/spree_cm_commissioner/integrations/inventory_sync_cachable.rb +9 -0
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +2 -1
- data/app/models/concerns/spree_cm_commissioner/order_prepaid_payment.rb +15 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +58 -8
- data/app/models/concerns/spree_cm_commissioner/percent_fee_calculable.rb +36 -0
- data/app/models/concerns/spree_cm_commissioner/product_type.rb +6 -2
- data/app/models/concerns/spree_cm_commissioner/reschedule_request_state_machine.rb +332 -0
- data/app/models/concerns/spree_cm_commissioner/scoped_fee_rule.rb +129 -0
- data/app/models/concerns/spree_cm_commissioner/service_type.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/store_preference.rb +1 -0
- data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +6 -0
- data/app/models/concerns/spree_cm_commissioner/variant_metadata.rb +24 -0
- data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +1 -1
- data/app/models/spree/core/controller_helpers/currency_decorator.rb +70 -0
- data/app/models/spree_cm_commissioner/adjustment_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/audit_event.rb +11 -0
- data/app/models/spree_cm_commissioner/calculators/payment_fee_flat_amount.rb +21 -0
- data/app/models/spree_cm_commissioner/calculators/payment_fee_percent.rb +17 -0
- data/app/models/spree_cm_commissioner/calculators/service_fee_flat_amount.rb +4 -0
- data/app/models/spree_cm_commissioner/calculators/service_fee_percent.rb +5 -2
- data/app/models/spree_cm_commissioner/configuration.rb +17 -9
- data/app/models/spree_cm_commissioner/currency_rate.rb +15 -0
- data/app/models/spree_cm_commissioner/current.rb +8 -0
- data/app/models/spree_cm_commissioner/customer_notification.rb +4 -1
- data/app/models/spree_cm_commissioner/exports/import_order_guests_csv.rb +153 -0
- data/app/models/spree_cm_commissioner/guest.rb +21 -0
- data/app/models/spree_cm_commissioner/integration.rb +31 -0
- data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +64 -0
- data/app/models/spree_cm_commissioner/integrations/larryta.rb +81 -0
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +4 -10
- data/app/models/spree_cm_commissioner/integrations/vireak_buntham.rb +82 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +22 -5
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +6 -2
- data/app/models/spree_cm_commissioner/notification.rb +8 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +31 -3
- data/app/models/spree_cm_commissioner/order_updater_decorator.rb +6 -1
- data/app/models/spree_cm_commissioner/payment_decorator.rb +31 -0
- data/app/models/spree_cm_commissioner/payment_fee_rule.rb +97 -0
- data/app/models/spree_cm_commissioner/payment_method_decorator.rb +5 -0
- data/app/models/spree_cm_commissioner/payment_method_store_credit_decorator.rb +81 -0
- data/app/models/spree_cm_commissioner/place.rb +2 -0
- data/app/models/spree_cm_commissioner/prepaid_config.rb +59 -0
- data/app/models/spree_cm_commissioner/prepaid_transaction.rb +102 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +6 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +4 -2
- data/app/models/spree_cm_commissioner/reschedule_policy.rb +94 -0
- data/app/models/spree_cm_commissioner/reschedule_policy_assignment.rb +19 -0
- data/app/models/spree_cm_commissioner/reschedule_request.rb +227 -0
- data/app/models/spree_cm_commissioner/reschedule_requests/guest_selection.rb +80 -0
- data/app/models/spree_cm_commissioner/reschedule_snapshot.rb +65 -0
- data/app/models/spree_cm_commissioner/route.rb +1 -0
- data/app/models/spree_cm_commissioner/seat_layout.rb +8 -0
- data/app/models/spree_cm_commissioner/service_calendar.rb +4 -0
- data/app/models/spree_cm_commissioner/service_fee_rule.rb +17 -114
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +1 -1
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +1 -1
- data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -0
- data/app/models/spree_cm_commissioner/store_credit_category_decorator.rb +23 -0
- data/app/models/spree_cm_commissioner/store_credit_decorator.rb +142 -0
- data/app/models/spree_cm_commissioner/store_credit_event_decorator.rb +94 -0
- data/app/models/spree_cm_commissioner/store_credit_type_decorator.rb +43 -0
- data/app/models/spree_cm_commissioner/store_decorator.rb +8 -0
- data/app/models/spree_cm_commissioner/tenant.rb +37 -0
- data/app/models/spree_cm_commissioner/trip.rb +2 -1
- data/app/models/spree_cm_commissioner/trip_stop.rb +2 -0
- data/app/models/spree_cm_commissioner/user_decorator.rb +79 -0
- data/app/models/spree_cm_commissioner/variant_decorator.rb +9 -1
- data/app/models/spree_cm_commissioner/vehicle.rb +3 -0
- data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +53 -0
- data/app/models/spree_cm_commissioner/vendor_place.rb +2 -0
- data/app/notifications/spree_cm_commissioner/reschedule_approved_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_cancelled_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_completed_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_expired_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_failed_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_notification_base.rb +41 -0
- data/app/notifications/spree_cm_commissioner/reschedule_payment_reminder_notification.rb +15 -0
- data/app/notifications/spree_cm_commissioner/reschedule_rejected_notification.rb +7 -0
- data/app/notifications/spree_cm_commissioner/reschedule_requested_notification.rb +7 -0
- data/app/overrides/spree/admin/products/_form/offline.html.erb.deface +9 -0
- data/app/overrides/spree/admin/shared/_main_menu/sidebar_reschedule.html.erb.deface +9 -0
- data/app/overrides/spree/admin/shared/_main_menu/sidebar_store_credit.html.erb.deface +8 -0
- data/app/overrides/spree/admin/shared/_order_tabs/reschedule_requests.html.erb.deface +10 -0
- data/app/overrides/spree/admin/shared/_product_tabs/stock_managements.html.erb.deface +1 -1
- data/app/overrides/spree/admin/shared/sub_menu/_configuration/menu_items.html.erb.deface +5 -0
- data/app/presenters/spree_cm_commissioner/prepaid_balance_presenter.rb +16 -0
- data/app/queries/spree_cm_commissioner/store_credit_overview_query.rb +75 -0
- data/app/queries/spree_cm_commissioner/user_store_credit_summary_query.rb +53 -0
- data/app/serializables/spree_cm_commissioner/item_availability_sync.rb +10 -0
- data/app/serializers/spree/v2/storefront/prepaid_balance_serializer.rb +15 -0
- data/app/serializers/spree/v2/storefront/prepaid_config_serializer.rb +11 -0
- data/app/serializers/spree/v2/storefront/prepaid_transaction_serializer.rb +17 -0
- data/app/serializers/spree/v2/storefront/user_serializer_decorator.rb +6 -0
- data/app/serializers/spree/v2/tenant/cart_serializer.rb +2 -1
- data/app/serializers/spree/v2/tenant/payment_method_serializer.rb +3 -0
- data/app/serializers/spree/v2/tenant/prepaid_balance_serializer.rb +15 -0
- data/app/serializers/spree/v2/tenant/prepaid_config_serializer.rb +16 -0
- data/app/serializers/spree/v2/tenant/prepaid_transaction_serializer.rb +16 -0
- data/app/serializers/spree_cm_commissioner/api/v2/platform/homepage_section_relatable_options_serializer.rb +5 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/cart_serializer_decorator.rb +3 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/inventory_item_serializer.rb +1 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/item_availability_sync_serializer.rb +10 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/payment_fee_preview_serializer.rb +19 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/payment_method_serializer_decorator.rb +16 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/reschedule_request_serializer.rb +49 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/reschedule_snapshot_serializer.rb +24 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/seat_layout_serializer.rb +8 -0
- data/app/services/spree_cm_commissioner/audit_logger.rb +59 -0
- data/app/services/spree_cm_commissioner/checkout/add_store_credit_decorator.rb +17 -0
- data/app/services/spree_cm_commissioner/checkout/add_store_credit_payments.rb +96 -0
- data/app/services/spree_cm_commissioner/checkout/remove_store_credit_decorator.rb +17 -0
- data/app/services/spree_cm_commissioner/currency_converter/create.rb +27 -0
- data/app/services/spree_cm_commissioner/currency_rates/manage.rb +99 -0
- data/app/services/spree_cm_commissioner/customer_notifications/clone.rb +39 -0
- data/app/services/spree_cm_commissioner/google_maps_geocoder.rb +41 -0
- data/app/services/spree_cm_commissioner/imports/orders/create.rb +24 -11
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +1 -2
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +11 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/client.rb +230 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/external_client/connection.rb +147 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_item_status.rb +52 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/external_inventory_items.rb +91 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/inventory/unstock_inventory.rb +188 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +354 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_locations.rb +103 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_nationality_pricing.rb +93 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +70 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_route_stops.rb +121 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_routes.rb +128 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_seat_layout.rb +412 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +59 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +224 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +769 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +92 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/time_parser.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/base.rb +40 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/inventory_item_status.rb +148 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/location.rb +55 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/reservation_cart.rb +85 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat.rb +128 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/seat_layout.rb +386 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/stop.rb +84 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +237 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/wallet_payment.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_manager.rb +22 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +116 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +63 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/external_client/client.rb +314 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/external_client/connection.rb +74 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_item_status.rb +53 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/external_inventory_items.rb +106 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/hold_seats.rb +143 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/release_seats.rb +90 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/inventory/unstock_inventory.rb +143 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +325 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +72 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_routes.rb +142 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_seat_layout.rb +255 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_stock_item.rb +55 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trip_stop.rb +171 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_trips.rb +397 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_variant.rb +63 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_vehicle_type.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/base.rb +39 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking.rb +113 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_session.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/booking_transaction.rb +236 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/direction.rb +71 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/passenger.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/schedule.rb +150 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat.rb +31 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_availability.rb +49 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/resources/seat_layout.rb +211 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_manager.rb +17 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/full_sync_strategy.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/larryta/sync_strategies/incremental_sync_strategy.rb +58 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +14 -15
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +7 -4
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +6 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +1 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +1 -3
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +1 -2
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/client.rb +170 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/external_client/connection.rb +59 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_item_status.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/external_inventory_items.rb +92 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/inventory/unstock_inventory.rb +230 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +270 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_locations.rb +96 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_nationality_pricing.rb +76 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_product.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_routes.rb +110 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_seat_layout.rb +207 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_stock_item.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trip_stop.rb +192 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_trips.rb +377 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_variant.rb +56 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_vehicle_type.rb +51 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/base.rb +32 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/booking_transaction.rb +46 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/cell.rb +53 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/confirm_booking_request.rb +43 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/location.rb +37 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/schedule.rb +138 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat.rb +48 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/resources/seat_layout.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_manager.rb +17 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/full_sync_strategy.rb +87 -0
- data/app/services/spree_cm_commissioner/integrations/vireak_buntham/sync_strategies/incremental_sync_strategy.rb +53 -0
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +13 -0
- data/app/services/spree_cm_commissioner/inventory_items/generate_permanent_item.rb +48 -0
- data/app/services/spree_cm_commissioner/order_holds/hold.rb +17 -0
- data/app/services/spree_cm_commissioner/order_holds/release.rb +31 -0
- data/app/services/spree_cm_commissioner/payment_fees/apply.rb +30 -0
- data/app/services/spree_cm_commissioner/payment_fees/clear.rb +15 -0
- data/app/services/spree_cm_commissioner/payment_fees/preview.rb +70 -0
- data/app/services/spree_cm_commissioner/payments/find_or_create_decorator.rb +18 -0
- data/app/services/spree_cm_commissioner/place_resolver.rb +95 -0
- data/app/services/spree_cm_commissioner/prepaid/admin_credit.rb +44 -0
- data/app/services/spree_cm_commissioner/prepaid/check_top_up_limits.rb +47 -0
- data/app/services/spree_cm_commissioner/prepaid/freeze.rb +58 -0
- data/app/services/spree_cm_commissioner/prepaid/mutation.rb +78 -0
- data/app/services/spree_cm_commissioner/prepaid/reverse_order_top_up.rb +23 -0
- data/app/services/spree_cm_commissioner/prepaid/reverse_top_up.rb +53 -0
- data/app/services/spree_cm_commissioner/prepaid/top_up.rb +68 -0
- data/app/services/spree_cm_commissioner/prepaid/unfreeze.rb +47 -0
- data/app/services/spree_cm_commissioner/prepaid_metrics.rb +37 -0
- data/app/services/spree_cm_commissioner/prepaid_reconciliation_service.rb +71 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/assign.rb +52 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/create.rb +71 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/exclude.rb +40 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/resolve.rb +109 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/unassign.rb +23 -0
- data/app/services/spree_cm_commissioner/reschedule_policies/update.rb +73 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/authorize_initiator.rb +79 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/build_request.rb +317 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/calculate_fee.rb +71 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/cancel.rb +55 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/check_eligibility.rb +243 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/collect_payment.rb +204 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/execute.rb +252 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/expire_overdue_requests.rb +28 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/expire_request.rb +24 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/guests/update_seats.rb +210 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/initiate.rb +255 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/issue_compensation.rb +67 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/issue_coupon.rb +90 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/issue_store_credit.rb +16 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_approval_reminder.rb +43 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_payment_reminder.rb +46 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/notifications/send_status_update.rb +42 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/seats/hold.rb +137 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/split_line_item.rb +86 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/telegram/notify_operations.rb +37 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/telegram/notify_vendor.rb +51 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/vendors/approve.rb +61 -0
- data/app/services/spree_cm_commissioner/reschedule_requests/vendors/reject.rb +58 -0
- data/app/services/spree_cm_commissioner/topup_draft_orders/create.rb +234 -0
- data/app/services/spree_cm_commissioner/trips/service_calendars/create_or_update.rb +25 -14
- data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +43 -6
- data/app/views/spree/admin/classifications/edit.html.erb +17 -2
- data/app/views/spree/admin/classifications/new.html.erb +18 -2
- data/app/views/spree/admin/customer_notifications/index.html.erb +45 -3
- data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +47 -0
- data/app/views/spree/admin/integrations/_form.html.erb +12 -0
- data/app/views/spree/admin/integrations/_larryta_fields.html.erb +58 -0
- data/app/views/spree/admin/integrations/_vireak_buntham_fields.html.erb +75 -0
- data/app/views/spree/admin/integrations/index.html.erb +1 -1
- data/app/views/spree/admin/payment_fee_rules/_form.html.erb +135 -0
- data/app/views/spree/admin/payment_fee_rules/edit.html.erb +11 -0
- data/app/views/spree/admin/payment_fee_rules/index.html.erb +100 -0
- data/app/views/spree/admin/payment_fee_rules/new.html.erb +11 -0
- data/app/views/spree/admin/prepaid_transactions/index.html.erb +102 -0
- data/app/views/spree/admin/prepaid_transactions/show.html.erb +146 -0
- data/app/views/spree/admin/private_sales/_form.html.erb +24 -1
- data/app/views/spree/admin/private_sales/_stock.html.erb +6 -1
- data/app/views/spree/admin/product_commissions/index.html.erb +7 -2
- data/app/views/spree/admin/reschedule_policies/_form.html.erb +65 -0
- data/app/views/spree/admin/reschedule_policies/edit.html.erb +11 -0
- data/app/views/spree/admin/reschedule_policies/index.html.erb +84 -0
- data/app/views/spree/admin/reschedule_policies/new.html.erb +11 -0
- data/app/views/spree/admin/reschedule_requests/_cancel_modal.html.erb +34 -0
- data/app/views/spree/admin/reschedule_requests/_search_form.html.erb +57 -0
- data/app/views/spree/admin/reschedule_requests/_tabs.html.erb +14 -0
- data/app/views/spree/admin/reschedule_requests/index.html.erb +64 -0
- data/app/views/spree/admin/reschedule_requests/show.html.erb +212 -0
- data/app/views/spree/admin/reschedule_requests/state_changes.html.erb +42 -0
- data/app/views/spree/admin/service_fee_rules/_form.html.erb +15 -9
- data/app/views/spree/admin/service_fee_rules/index.html.erb +1 -1
- data/app/views/spree/admin/shared/_products.html.erb +3 -0
- data/app/views/spree/admin/shared/_reschedule_policies_tabs.html.erb +22 -0
- data/app/views/spree/admin/shared/_ticket_transfer_details.html.erb +3 -0
- data/app/views/spree/admin/shared/sub_menu/_reschedule.html.erb +4 -0
- data/app/views/spree/admin/stock_managements/_header.html.erb +26 -0
- data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +1 -1
- data/app/views/spree/admin/stock_managements/calendar.html.erb +285 -171
- data/app/views/spree/admin/stock_managements/index.html.erb +1 -18
- data/app/views/spree/admin/store_credit_admin/_tabs.html.erb +31 -0
- data/app/views/spree/admin/store_credit_admin/_tenant_selector.html.erb +17 -0
- data/app/views/spree/admin/store_credit_admin/show.html.erb +200 -0
- data/app/views/spree/admin/store_credits/index.html.erb +224 -0
- data/app/views/spree/admin/tenants/_form.html.erb +33 -0
- data/app/views/spree/admin/ticket_transfer_rules/_form.html.erb +28 -5
- data/config/initializers/spree_dependencies.rb +3 -0
- data/config/initializers/spree_permitted_attributes.rb +1 -0
- data/config/locales/en.yml +363 -1
- data/config/locales/km.yml +282 -1
- data/config/routes.rb +67 -2
- data/db/migrate/20260327143200_create_cm_audit_events.rb +21 -0
- data/db/migrate/20260410045815_create_cm_currency_rates.rb +23 -0
- data/db/migrate/20260411102139_add_metadata_to_cm_inventory_items.rb +6 -0
- data/db/migrate/20260506120000_create_cm_reschedule_policies.rb +38 -0
- data/db/migrate/20260506120001_create_cm_reschedule_requests.rb +67 -0
- data/db/migrate/20260506120002_add_reschedule_count_to_cm_guests.rb +5 -0
- data/db/migrate/20260521140000_create_cm_reschedule_snapshots.rb +38 -0
- data/db/migrate/20260522160000_create_cm_reschedule_policy_assignments.rb +17 -0
- data/db/migrate/20260722000001_create_cm_prepaid_transactions.rb +31 -0
- data/db/migrate/20260722000002_add_constraints_to_store_credits.rb +46 -0
- data/db/migrate/20260722000003_add_expires_at_to_store_credits.rb +6 -0
- data/db/migrate/20260722000004_add_issuer_tenant_id_to_spree_store_credits.rb +7 -0
- data/db/migrate/20260722000005_add_immutability_trigger_to_cm_prepaid_transactions.rb +36 -0
- data/db/migrate/20260918045558_add_prepaid_currency_columns_to_cm_tenants.rb +6 -0
- data/db/migrate/20260923090000_add_public_private_metadata_to_cm_prepaid_transactions.rb +36 -0
- data/db/migrate/20260928000001_add_closed_to_cm_inventory_items.rb +5 -0
- data/db/migrate/20261001100000_create_cm_payment_fee_rules.rb +17 -0
- data/db/migrate/20261001100001_add_payment_fee_total_to_spree_orders.rb +5 -0
- data/db/migrate/20261006073317_add_offline_to_spree_products.rb +6 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +7 -1
- data/lib/spree_cm_commissioner/payment_fees/payment_fee_preview.rb +24 -0
- data/lib/spree_cm_commissioner/reschedule_policy_rules.rb +75 -0
- data/lib/spree_cm_commissioner/serializable_transaction.rb +38 -0
- data/lib/spree_cm_commissioner/store_credit_currency_summary.rb +11 -0
- data/lib/spree_cm_commissioner/store_credit_summary.rb +11 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +11 -0
- data/lib/spree_cm_commissioner/test_helper/factories/prepaid_transaction_factory.rb +12 -0
- data/lib/spree_cm_commissioner/test_helper/factories/reschedule_policy_assignment_factory.rb +7 -0
- data/lib/spree_cm_commissioner/test_helper/factories/reschedule_policy_factory.rb +59 -0
- data/lib/spree_cm_commissioner/test_helper/factories/reschedule_request_factory.rb +51 -0
- data/lib/spree_cm_commissioner/test_helper/factories/reschedule_snapshot_factory.rb +27 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +5 -0
- data/lib/tasks/prepaid_reconciliation_cron_scheduler.rake +7 -0
- data/lib/tasks/prepaid_setup_types.rake +7 -0
- metadata +295 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module PaymentFees
|
|
3
|
+
# Replaces the order's payment fee with what PaymentFees::Preview computes for
|
|
4
|
+
# `payment_method`, storing those exact amounts (same pattern as PricingModels::Activate):
|
|
5
|
+
# never recomputed through PaymentFeeRule#compute_amount, and closed so a later
|
|
6
|
+
# Adjustment#update! can't change them. Caller is expected to hold the transaction.
|
|
7
|
+
class Apply
|
|
8
|
+
prepend ::Spree::ServiceModule::Base
|
|
9
|
+
|
|
10
|
+
def call(order:, payment_method:)
|
|
11
|
+
order.adjustments.payment_fee.delete_all
|
|
12
|
+
|
|
13
|
+
preview = Preview.call(order: order, payment_method: payment_method).value[:payment_fee_preview]
|
|
14
|
+
preview.adjustments.each do |adjustment|
|
|
15
|
+
order.adjustments.create!(
|
|
16
|
+
amount: adjustment.amount,
|
|
17
|
+
source: adjustment.rule,
|
|
18
|
+
order: order,
|
|
19
|
+
label: adjustment.rule.label,
|
|
20
|
+
mandatory: true,
|
|
21
|
+
state: 'closed'
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
order.update_with_updater!
|
|
26
|
+
success(order: order, payment_fee_preview: preview)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module PaymentFees
|
|
3
|
+
class Clear
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:)
|
|
7
|
+
return success(order: order) unless order.adjustments.payment_fee.exists?
|
|
8
|
+
|
|
9
|
+
order.adjustments.payment_fee.delete_all
|
|
10
|
+
order.update_with_updater!
|
|
11
|
+
success(order: order)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module PaymentFees
|
|
3
|
+
# Computes, without persisting, the payment fee `payment_method` would add to `order`.
|
|
4
|
+
#
|
|
5
|
+
# The base is the gateway amount only (order total minus store credit, minus any payment fee
|
|
6
|
+
# already applied from an earlier attempt), since that's what the provider bills on. Line items
|
|
7
|
+
# resolve their own rule (product -> event -> vendor -> tenant/global); when they resolve to
|
|
8
|
+
# different rules, the gateway amount is split between rules by each group's share of line-item
|
|
9
|
+
# totals, and the leftover rounding cent goes to the last group so the bases always add up.
|
|
10
|
+
class Preview
|
|
11
|
+
prepend ::Spree::ServiceModule::Base
|
|
12
|
+
|
|
13
|
+
def call(order:, payment_method:)
|
|
14
|
+
gateway_amount = gateway_amount_for(order)
|
|
15
|
+
adjustments = build_adjustments(order, payment_method, gateway_amount)
|
|
16
|
+
amount = adjustments.sum(BigDecimal(0), &:amount)
|
|
17
|
+
|
|
18
|
+
success(
|
|
19
|
+
payment_fee_preview: PaymentFeePreview.new(
|
|
20
|
+
payment_method_id: payment_method.id,
|
|
21
|
+
currency: order.currency,
|
|
22
|
+
amount: amount,
|
|
23
|
+
order_total_after_store_credit: gateway_amount + amount,
|
|
24
|
+
fee_description: adjustments.one? ? adjustments.first.rule.fee_description : nil,
|
|
25
|
+
adjustments: adjustments
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def gateway_amount_for(order)
|
|
33
|
+
[order.order_total_after_store_credit - order.payment_fee_total, 0].max
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_adjustments(order, payment_method, gateway_amount)
|
|
37
|
+
return [] unless gateway_amount.positive?
|
|
38
|
+
|
|
39
|
+
groups = order.line_items.group_by do |line_item|
|
|
40
|
+
PaymentFeeRule.resolve_for(line_item: line_item, payment_method: payment_method, currency: order.currency)
|
|
41
|
+
end
|
|
42
|
+
groups.delete(nil)
|
|
43
|
+
return [] if groups.empty?
|
|
44
|
+
|
|
45
|
+
bases = split(gateway_amount, groups, order)
|
|
46
|
+
groups.keys.zip(bases).filter_map do |rule, base|
|
|
47
|
+
amount = round(rule.calculator&.compute(base) || 0, order)
|
|
48
|
+
PaymentFeePreview::Adjustment.new(rule: rule, amount: amount) if amount.positive?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Items with no rule still pay their share of the gateway amount, just without a fee, so
|
|
53
|
+
# weights are taken over every line item, not only the ruled ones.
|
|
54
|
+
def split(gateway_amount, groups, order)
|
|
55
|
+
all_items_total = order.line_items.sum(&:total)
|
|
56
|
+
return [gateway_amount] + Array.new(groups.size - 1, 0) unless all_items_total.positive?
|
|
57
|
+
|
|
58
|
+
bases = groups.values.map { |items| round(gateway_amount * items.sum(&:total) / all_items_total, order) }
|
|
59
|
+
return bases if groups.size < order.line_items.size
|
|
60
|
+
|
|
61
|
+
bases[-1] += gateway_amount - bases.sum
|
|
62
|
+
bases
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def round(amount, order)
|
|
66
|
+
amount.to_d.round(::Money::Currency.new(order.currency).exponent)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Payments
|
|
3
|
+
module FindOrCreateDecorator
|
|
4
|
+
# override: applies the selected method's payment fee before the payment is built, so
|
|
5
|
+
# `amount: order.order_total_after_store_credit` already includes it. Runs inside
|
|
6
|
+
# FindOrCreate's transaction, so a failed payment save rolls the fee back with it.
|
|
7
|
+
def find_or_create_payment(order:, params:, payment_method:)
|
|
8
|
+
SpreeCmCommissioner::PaymentFees::Apply.call(order: order, payment_method: payment_method)
|
|
9
|
+
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
unless Vpago::Payments::FindOrCreate.included_modules.include?(SpreeCmCommissioner::Payments::FindOrCreateDecorator)
|
|
17
|
+
Vpago::Payments::FindOrCreate.prepend(SpreeCmCommissioner::Payments::FindOrCreateDecorator)
|
|
18
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# PlaceResolver finds or builds a SpreeCmCommissioner::Place keyed on a stable
|
|
3
|
+
# upstream identifier instead of the fragile name lookup the polling services
|
|
4
|
+
# used to do (`Place.find_by(name:)`).
|
|
5
|
+
#
|
|
6
|
+
# Lookup order:
|
|
7
|
+
# 1. IntegrationMapping for (integration, external_id) on Place — authoritative
|
|
8
|
+
# 2. Legacy backfill: Place.find_by(name:) — adopts pre-mapping rows on first re-sync
|
|
9
|
+
# 3. Build a new Place
|
|
10
|
+
#
|
|
11
|
+
# The resolver persists the Place and its IntegrationMapping. Callers then
|
|
12
|
+
# wire the Place to a VendorPlace (which has its own mapping).
|
|
13
|
+
#
|
|
14
|
+
# `Place#reference` is left to the Google Places geocoder — it's the
|
|
15
|
+
# canonical Google place_id used by admin tooling and vendor place selection.
|
|
16
|
+
class PlaceResolver
|
|
17
|
+
def initialize(integration:, sync_result: nil, geocoder: nil)
|
|
18
|
+
@integration = integration
|
|
19
|
+
@sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
|
|
20
|
+
@geocoder = geocoder || SpreeCmCommissioner::GoogleMapsGeocoder.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param external_id [String] stable upstream id, e.g. "location-1", "stop-3", "branch-65"
|
|
24
|
+
# @param name [String] display name
|
|
25
|
+
# @param code [String, nil] short code
|
|
26
|
+
# @param lat [Numeric, nil] upstream latitude when available (preferred over geocoding)
|
|
27
|
+
# @param lon [Numeric, nil] upstream longitude when available
|
|
28
|
+
# @param geocode_context [String, nil] free-form context for geocoder fallback (e.g. country name)
|
|
29
|
+
# @return [SpreeCmCommissioner::Place]
|
|
30
|
+
def resolve(external_id:, name:, code: nil, lat: nil, lon: nil, geocode_context: nil) # rubocop:disable Metrics/ParameterLists
|
|
31
|
+
mapping = SpreeCmCommissioner::Place.find_or_initialize_integration_mapping(
|
|
32
|
+
integration_id: @integration.id,
|
|
33
|
+
external_id: external_id
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
place = resolve_place(mapping, name: name)
|
|
37
|
+
ensure_coordinates!(place, lat: lat, lon: lon, geocode_context: geocode_context, geocode_query: name)
|
|
38
|
+
|
|
39
|
+
@sync_result.track(:place, place) do |tracker|
|
|
40
|
+
attrs = { name: name }
|
|
41
|
+
attrs[:code] = code if code.present?
|
|
42
|
+
place.assign_attributes(attrs)
|
|
43
|
+
tracker.save_if_changed!(place)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
finalize_mapping!(mapping, place: place, name: name)
|
|
47
|
+
place
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Pick the Place this mapping should point at: existing internal, legacy adopted, or new.
|
|
53
|
+
# Adoption-by-name only adopts a Place that does NOT already have an
|
|
54
|
+
# IntegrationMapping for this integration — otherwise two upstream ids with
|
|
55
|
+
# the same display name would both adopt the same Place row.
|
|
56
|
+
def resolve_place(mapping, name:)
|
|
57
|
+
return mapping.internal if mapping.internal&.persisted?
|
|
58
|
+
|
|
59
|
+
legacy = SpreeCmCommissioner::Place.find_by(name: name)
|
|
60
|
+
if legacy && !already_mapped?(legacy)
|
|
61
|
+
mapping.internal = legacy
|
|
62
|
+
legacy
|
|
63
|
+
else
|
|
64
|
+
mapping.internal
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def already_mapped?(place)
|
|
69
|
+
SpreeCmCommissioner::IntegrationMapping.exists?(
|
|
70
|
+
integration_id: @integration.id,
|
|
71
|
+
internal_type: 'SpreeCmCommissioner::Place',
|
|
72
|
+
internal_id: place.id
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ensure_coordinates!(place, lat:, lon:, geocode_context:, geocode_query:) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
77
|
+
place.lat ||= lat if lat.present?
|
|
78
|
+
place.lon ||= lon if lon.present?
|
|
79
|
+
|
|
80
|
+
return if place.lat.present? && place.lon.present? && place.reference.present?
|
|
81
|
+
# Only geocode for brand-new Places. Persisted rows keep whatever coords they already have.
|
|
82
|
+
return unless place.new_record?
|
|
83
|
+
|
|
84
|
+
location_data = @geocoder.fetch_location_data(geocode_query, geocode_context) || {}
|
|
85
|
+
place.lat = location_data[:lat] if place.lat.blank?
|
|
86
|
+
place.lon = location_data[:lon] if place.lon.blank?
|
|
87
|
+
place.reference = location_data[:reference] if place.reference.blank?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def finalize_mapping!(mapping, place:, name:)
|
|
91
|
+
mapping.internal = place if mapping.internal != place
|
|
92
|
+
mapping.mark_as_active!(external_payload: { name: name })
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Issues a goodwill/support credit to a user's prepaid balance. Distinct from TopUp only
|
|
4
|
+
# in who initiates it (an admin, not a payment) and the ledger ends up storing `reason`.
|
|
5
|
+
class AdminCredit
|
|
6
|
+
prepend ::Spree::ServiceModule::Base
|
|
7
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
8
|
+
include SpreeCmCommissioner::Prepaid::Mutation
|
|
9
|
+
|
|
10
|
+
# rubocop:disable Metrics/ParameterLists
|
|
11
|
+
def call(user:, amount:, currency:, idempotency_key:, reason:, issuer_tenant_id: nil, admin: nil, private_metadata: {})
|
|
12
|
+
return failure(nil, 'amount must be positive') unless amount.to_d.positive?
|
|
13
|
+
return failure(nil, 'unsupported currency') unless Spree::Store.default.supported_currency_codes.include?(currency)
|
|
14
|
+
return failure(nil, 'reason is required') if reason.blank?
|
|
15
|
+
return failure(nil, 'admin is required') if admin.nil?
|
|
16
|
+
|
|
17
|
+
store_credit = run_serializable do
|
|
18
|
+
credit = find_or_create_prepaid_credit!(
|
|
19
|
+
user: user, currency: currency, issuer_tenant_id: issuer_tenant_id,
|
|
20
|
+
created_by: admin, initial_amount: amount.to_d
|
|
21
|
+
)
|
|
22
|
+
check_not_topup_blocked!(credit)
|
|
23
|
+
|
|
24
|
+
credit.update!(amount: credit.amount + amount.to_d) unless credit.previously_new_record?
|
|
25
|
+
write_ledger!(
|
|
26
|
+
store_credit: credit,
|
|
27
|
+
transaction_type: 'admin_credit',
|
|
28
|
+
amount: amount.to_d,
|
|
29
|
+
idempotency_key: idempotency_key,
|
|
30
|
+
tenant_id: issuer_tenant_id,
|
|
31
|
+
admin: admin,
|
|
32
|
+
private_metadata: private_metadata.merge(reason: reason)
|
|
33
|
+
)
|
|
34
|
+
credit
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
success(store_credit)
|
|
38
|
+
rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction, SpreeCmCommissioner::Prepaid::PrepaidFrozen => e
|
|
39
|
+
failure(nil, e.message)
|
|
40
|
+
end
|
|
41
|
+
# rubocop:enable Metrics/ParameterLists
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Enforces top-up velocity/daily limits (hourly count, daily count, daily amount) --
|
|
4
|
+
# extracted from TopUp so each limit is independently unit-testable instead of only
|
|
5
|
+
# reachable through the full money-in transaction.
|
|
6
|
+
# docs/concepts/store-credit-wallet-system.md Section 8 -- platform-wide for now (the doc's
|
|
7
|
+
# per-tenant PrepaidConfigResolver was never built). A limit of 0 means "no limit", same
|
|
8
|
+
# convention the doc uses. Only user-scoped checks; the doc's IP-based "5 distinct users/hour
|
|
9
|
+
# from one IP" check isn't implemented here -- this has no request context to check it from.
|
|
10
|
+
class CheckTopUpLimits
|
|
11
|
+
prepend ::Spree::ServiceModule::Base
|
|
12
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
13
|
+
|
|
14
|
+
def call(user:, amount:, currency:)
|
|
15
|
+
config = SpreeCmCommissioner::Configuration.new
|
|
16
|
+
completed_topups = SpreeCmCommissioner::PrepaidTransaction.completed.where(user: user, transaction_type: 'top_up')
|
|
17
|
+
|
|
18
|
+
# Count-based checks (velocity/fraud signal, not money) deliberately stay currency-agnostic --
|
|
19
|
+
# 5 top-ups split across USD and KHR is still 5 top-ups.
|
|
20
|
+
hourly_limit = config.prepaid_max_topups_per_hour.to_i
|
|
21
|
+
if hourly_limit.positive? && completed_topups.where(created_at: 1.hour.ago..).count >= hourly_limit
|
|
22
|
+
return failure(nil, "too many top-ups in the last hour (max #{hourly_limit})")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
todays_topups = completed_topups.where(created_at: Time.current.beginning_of_day..)
|
|
26
|
+
|
|
27
|
+
daily_count_limit = config.prepaid_max_topup_count_daily.to_i
|
|
28
|
+
if daily_count_limit.positive? && todays_topups.count >= daily_count_limit
|
|
29
|
+
return failure(nil, "daily top-up count limit reached (max #{daily_count_limit})")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Amount-based check must be scoped to this currency and the limit scaled into it (same
|
|
33
|
+
# rate PrepaidConfig#build_currency_limits shows the client) -- KHR amounts run ~4100x
|
|
34
|
+
# USD's, so summing across currencies or comparing against the raw USD-denominated
|
|
35
|
+
# config value would reject (or wrongly allow) almost every non-preferred-currency top-up.
|
|
36
|
+
rate = config.prepaid_currency_rates[currency].to_f
|
|
37
|
+
daily_amount_limit = config.prepaid_daily_topup_limit.to_d * rate
|
|
38
|
+
todays_amount = todays_topups.where(currency: currency).sum(:amount)
|
|
39
|
+
if daily_amount_limit.positive? && todays_amount + amount > daily_amount_limit
|
|
40
|
+
return failure(nil, "daily top-up amount limit exceeded (max #{daily_amount_limit})")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
success(nil)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Freezes a store credit so PrepaidService money-in operations (and, at 'full' level,
|
|
4
|
+
# Spree's own spend path via StoreCreditDecorator#spend_blocked?) are blocked.
|
|
5
|
+
class Freeze
|
|
6
|
+
prepend ::Spree::ServiceModule::Base
|
|
7
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
8
|
+
include Mutation
|
|
9
|
+
|
|
10
|
+
LEVELS = %w[full topup_only].freeze
|
|
11
|
+
|
|
12
|
+
def call(store_credit:, admin:, reason:, level: 'full', until_at: nil)
|
|
13
|
+
return failure(nil, 'reason is required') if reason.blank?
|
|
14
|
+
return failure(nil, 'invalid freeze level') unless LEVELS.include?(level)
|
|
15
|
+
|
|
16
|
+
# Locked and transactional so a freeze can't interleave with a concurrent money-in
|
|
17
|
+
# operation (AdminCredit/TopUp lock the same row via Mutation#find_or_create_prepaid_credit!)
|
|
18
|
+
# -- otherwise a top-up could land in the gap between reading and writing the freeze columns.
|
|
19
|
+
store_credit = run_serializable do
|
|
20
|
+
credit = Spree::StoreCredit.lock.find(store_credit.id)
|
|
21
|
+
|
|
22
|
+
# A re-freeze (credit was already frozen, e.g. reason/level changed before ever being
|
|
23
|
+
# lifted) would otherwise overwrite frozen_at/freeze_reason/freeze_level with no trace
|
|
24
|
+
# of what the prior freeze was -- capture it here, same as Unfreeze does when a freeze
|
|
25
|
+
# is lifted.
|
|
26
|
+
write_ledger!(
|
|
27
|
+
store_credit: credit,
|
|
28
|
+
transaction_type: 'freeze',
|
|
29
|
+
amount: 0,
|
|
30
|
+
idempotency_key: "freeze:#{credit.id}:#{Time.current.to_i / 5}",
|
|
31
|
+
admin: admin,
|
|
32
|
+
private_metadata: {
|
|
33
|
+
reason: reason,
|
|
34
|
+
level: level,
|
|
35
|
+
until_at: until_at,
|
|
36
|
+
prior_frozen_by_id: credit.frozen_by_id,
|
|
37
|
+
prior_freeze_reason: credit.freeze_reason,
|
|
38
|
+
prior_freeze_level: credit.freeze_level
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
credit.update!(
|
|
43
|
+
frozen_at: Time.current,
|
|
44
|
+
frozen_by: admin,
|
|
45
|
+
freeze_reason: reason,
|
|
46
|
+
freeze_level: level,
|
|
47
|
+
frozen_until: until_at
|
|
48
|
+
)
|
|
49
|
+
credit
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
success(store_credit)
|
|
53
|
+
rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction => e
|
|
54
|
+
failure(nil, e.message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Raised when a caller-supplied idempotency_key has already been used for a completed
|
|
4
|
+
# ledger row. Callers (jobs, other services) should rescue this and treat it as a no-op.
|
|
5
|
+
DuplicateTransaction = Class.new(StandardError)
|
|
6
|
+
|
|
7
|
+
# Raised when a money-in operation is attempted against frozen store credit.
|
|
8
|
+
PrepaidFrozen = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
# Shared plumbing for the money-in services (TopUp, AdminCredit, ReverseTopUp): opening
|
|
11
|
+
# a serializable transaction, finding/creating the per-(user, currency, issuer_tenant_id)
|
|
12
|
+
# prepaid credit under a row lock, and writing the ledger row that mirrors the mutation.
|
|
13
|
+
# Not a service object itself -- included into the services that are.
|
|
14
|
+
module Mutation
|
|
15
|
+
def run_serializable(&block)
|
|
16
|
+
SpreeCmCommissioner::SerializableTransaction.run(&block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Spree::StoreCredit requires `amount > 0`, so a brand-new credit is created with
|
|
20
|
+
# `initial_amount` already applied rather than created at 0 and then topped up --
|
|
21
|
+
# callers must only add `amount` on top for a credit that already existed.
|
|
22
|
+
def find_or_create_prepaid_credit!(user:, currency:, issuer_tenant_id:, created_by:, initial_amount:)
|
|
23
|
+
credit_type = Spree::StoreCreditType.prepaid!
|
|
24
|
+
|
|
25
|
+
credit = Spree::StoreCredit.lock.find_by(
|
|
26
|
+
user_id: user.id,
|
|
27
|
+
currency: currency,
|
|
28
|
+
issuer_tenant_id: issuer_tenant_id,
|
|
29
|
+
type_id: credit_type.id
|
|
30
|
+
)
|
|
31
|
+
return credit if credit
|
|
32
|
+
|
|
33
|
+
Spree::StoreCredit.create!(
|
|
34
|
+
user: user,
|
|
35
|
+
currency: currency,
|
|
36
|
+
issuer_tenant_id: issuer_tenant_id,
|
|
37
|
+
credit_type: credit_type,
|
|
38
|
+
category: Spree::StoreCreditCategory.wallet!,
|
|
39
|
+
created_by: created_by,
|
|
40
|
+
amount: initial_amount,
|
|
41
|
+
store: Spree::Store.default
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# rubocop:disable Metrics/ParameterLists
|
|
46
|
+
# admin is a real column (who performed an admin-initiated action), separate from
|
|
47
|
+
# private_metadata (freeform detail) -- see PrepaidTransaction#admin/store_private_metadata
|
|
48
|
+
# declarations for why. Named private_metadata, not the ambiguous bare `metadata`, since
|
|
49
|
+
# PrepaidTransaction also has a separate public_metadata column this never writes to.
|
|
50
|
+
def write_ledger!(store_credit:, transaction_type:, amount:, idempotency_key:, tenant_id: nil, order: nil, admin: nil, private_metadata: {})
|
|
51
|
+
SpreeCmCommissioner::PrepaidTransaction.create!(
|
|
52
|
+
user: store_credit.user,
|
|
53
|
+
store_credit: store_credit,
|
|
54
|
+
transaction_type: transaction_type,
|
|
55
|
+
amount: amount,
|
|
56
|
+
balance_after: store_credit.amount,
|
|
57
|
+
currency: store_credit.currency,
|
|
58
|
+
idempotency_key: idempotency_key,
|
|
59
|
+
tenant_id: tenant_id,
|
|
60
|
+
order: order,
|
|
61
|
+
admin: admin,
|
|
62
|
+
private_metadata: private_metadata
|
|
63
|
+
)
|
|
64
|
+
rescue ActiveRecord::RecordNotUnique
|
|
65
|
+
raise DuplicateTransaction, "idempotency_key already used: #{idempotency_key}"
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Metrics/ParameterLists
|
|
68
|
+
|
|
69
|
+
def check_not_topup_blocked!(store_credit)
|
|
70
|
+
raise PrepaidFrozen, 'store credit is frozen for top-up' if store_credit.topup_blocked?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_not_spend_blocked!(store_credit)
|
|
74
|
+
raise PrepaidFrozen, 'store credit is frozen for spend' if store_credit.spend_blocked?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Reverses a top-up order's credit in one call -- looks up the order's top_up ledger row
|
|
4
|
+
# itself and delegates to ReverseTopUp, so callers (PrepaidReverseTopUpJob, etc.) only need
|
|
5
|
+
# to pass the order instead of first finding the transaction and its store_credit themselves.
|
|
6
|
+
class ReverseOrderTopUp
|
|
7
|
+
prepend ::Spree::ServiceModule::Base
|
|
8
|
+
|
|
9
|
+
def call(order:, reason: nil, private_metadata: {})
|
|
10
|
+
txn = SpreeCmCommissioner::PrepaidTransaction.find_by(order: order, transaction_type: 'top_up')
|
|
11
|
+
return failure(nil, 'no top-up transaction found for order') if txn.nil?
|
|
12
|
+
|
|
13
|
+
SpreeCmCommissioner::Prepaid::ReverseTopUp.call(
|
|
14
|
+
store_credit: txn.store_credit,
|
|
15
|
+
amount: order.total,
|
|
16
|
+
idempotency_key: "cancel:order:#{order.number}",
|
|
17
|
+
reason: reason,
|
|
18
|
+
private_metadata: private_metadata
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Claws back a prepaid credit, capped at the unspent amount -- amount_used can never be
|
|
4
|
+
# reduced, so a partially-spent top-up can only be reversed for what remains available.
|
|
5
|
+
# The ledger records the amount actually reversed (which may be less than requested) so
|
|
6
|
+
# reconciliation reconstructs the balance exactly.
|
|
7
|
+
class ReverseTopUp
|
|
8
|
+
prepend ::Spree::ServiceModule::Base
|
|
9
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
10
|
+
include SpreeCmCommissioner::Prepaid::Mutation
|
|
11
|
+
|
|
12
|
+
NothingToReverse = Class.new(StandardError)
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists,Lint/MissingCopEnableDirective
|
|
15
|
+
# admin is optional, not required (unlike AdminCredit) -- this service has a second,
|
|
16
|
+
# system-initiated caller (ReverseOrderTopUp, via PrepaidReverseTopUpJob, reversing a
|
|
17
|
+
# top-up order's credit on cancellation) with no admin in context at all.
|
|
18
|
+
def call(store_credit:, amount:, idempotency_key:, admin: nil, reason: nil, private_metadata: {})
|
|
19
|
+
return failure(nil, 'amount must be positive') unless amount.to_d.positive?
|
|
20
|
+
|
|
21
|
+
store_credit = run_serializable do
|
|
22
|
+
# Deliberately no check_not_topup_blocked!/check_not_spend_blocked! here: freezing a
|
|
23
|
+
# wallet blocks the *account holder* from topping up or spending -- it must never block
|
|
24
|
+
# this administrative/system clawback, or freezing a wallet during a fraud review would
|
|
25
|
+
# actually protect the fraudulent credit from being reversed.
|
|
26
|
+
credit = Spree::StoreCredit.lock.find(store_credit.id)
|
|
27
|
+
|
|
28
|
+
available = credit.amount - credit.amount_used - credit.amount_authorized
|
|
29
|
+
reversed = [amount.to_d, available].min
|
|
30
|
+
raise NothingToReverse if reversed <= 0
|
|
31
|
+
|
|
32
|
+
credit.update!(amount: credit.amount - reversed)
|
|
33
|
+
write_ledger!(
|
|
34
|
+
store_credit: credit,
|
|
35
|
+
transaction_type: 'reverse_top_up',
|
|
36
|
+
amount: -reversed,
|
|
37
|
+
idempotency_key: idempotency_key,
|
|
38
|
+
tenant_id: credit.issuer_tenant_id,
|
|
39
|
+
admin: admin,
|
|
40
|
+
private_metadata: private_metadata.merge(reason: reason, requested_amount: amount.to_d, reversed_amount: reversed)
|
|
41
|
+
)
|
|
42
|
+
credit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
success(store_credit)
|
|
46
|
+
rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction => e
|
|
47
|
+
failure(nil, e.message)
|
|
48
|
+
rescue NothingToReverse
|
|
49
|
+
failure(nil, 'nothing available to reverse')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
# Credits a user's prepaid store credit balance (real-money top-up). One writer path
|
|
4
|
+
# shared with AdminCredit/ReverseTopUp via Prepaid::Mutation -- see
|
|
5
|
+
# docs/concepts/store-credit-wallet-system.md Section 4.
|
|
6
|
+
class TopUp
|
|
7
|
+
prepend ::Spree::ServiceModule::Base
|
|
8
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
9
|
+
include SpreeCmCommissioner::Prepaid::Mutation
|
|
10
|
+
|
|
11
|
+
LimitExceeded = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
|
14
|
+
def call(user:, amount:, currency:, idempotency_key:, issuer_tenant_id: nil, order: nil, private_metadata: {})
|
|
15
|
+
return failure(nil, 'amount must be positive') unless amount.to_d.positive?
|
|
16
|
+
return failure(nil, 'unsupported currency') unless Spree::Store.default.supported_currency_codes.include?(currency)
|
|
17
|
+
|
|
18
|
+
store_credit = run_serializable do
|
|
19
|
+
SpreeCmCommissioner::Prepaid::CheckTopUpLimits.call!(user: user, amount: amount.to_d, currency: currency)
|
|
20
|
+
|
|
21
|
+
# The paying customer authorized this via their own payment -- no admin/system
|
|
22
|
+
# account involved, so they're the natural `created_by` (Spree only requires it be
|
|
23
|
+
# *a* Spree::User; nothing gates it to an admin role).
|
|
24
|
+
credit = find_or_create_prepaid_credit!(
|
|
25
|
+
user: user, currency: currency, issuer_tenant_id: issuer_tenant_id,
|
|
26
|
+
created_by: user, initial_amount: amount.to_d
|
|
27
|
+
)
|
|
28
|
+
check_not_topup_blocked!(credit)
|
|
29
|
+
check_max_balance!(credit, amount.to_d)
|
|
30
|
+
|
|
31
|
+
credit.update!(amount: credit.amount + amount.to_d) unless credit.previously_new_record?
|
|
32
|
+
write_ledger!(
|
|
33
|
+
store_credit: credit,
|
|
34
|
+
transaction_type: 'top_up',
|
|
35
|
+
amount: amount.to_d,
|
|
36
|
+
idempotency_key: idempotency_key,
|
|
37
|
+
tenant_id: issuer_tenant_id,
|
|
38
|
+
order: order,
|
|
39
|
+
private_metadata: private_metadata
|
|
40
|
+
)
|
|
41
|
+
credit
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
success(store_credit)
|
|
45
|
+
rescue SpreeCmCommissioner::Prepaid::DuplicateTransaction, SpreeCmCommissioner::Prepaid::PrepaidFrozen,
|
|
46
|
+
SpreeCmCommissioner::ServiceFailure, LimitExceeded => e
|
|
47
|
+
failure(nil, e.message)
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/ParameterLists
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# max_balance is denominated in Spree::Store.default#default_currency -- scale it into
|
|
54
|
+
# credit.currency's own magnitude (same PrepaidConfig#build_currency_limits formula) before
|
|
55
|
+
# comparing, or a KHR wallet's ~4100x-larger raw numbers would trip the USD-sized cap
|
|
56
|
+
# almost immediately.
|
|
57
|
+
def check_max_balance!(credit, amount)
|
|
58
|
+
config = SpreeCmCommissioner::Configuration.new
|
|
59
|
+
rate = config.prepaid_currency_rates[credit.currency].to_f
|
|
60
|
+
max_balance = config.prepaid_max_balance.to_d * rate
|
|
61
|
+
return unless max_balance.positive?
|
|
62
|
+
return unless credit.amount + amount > max_balance
|
|
63
|
+
|
|
64
|
+
raise LimitExceeded, "store credit balance cap exceeded (max #{max_balance})"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Prepaid
|
|
3
|
+
class Unfreeze
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
extend SpreeCmCommissioner::ServiceModuleThrowable
|
|
6
|
+
include Mutation
|
|
7
|
+
|
|
8
|
+
def call(store_credit:, admin: nil)
|
|
9
|
+
# Same locking rationale as Freeze#call -- avoid interleaving with a concurrent money-in
|
|
10
|
+
# operation that locks the same row.
|
|
11
|
+
store_credit = run_serializable do
|
|
12
|
+
credit = Spree::StoreCredit.lock.find(store_credit.id)
|
|
13
|
+
|
|
14
|
+
# Freeze columns are about to be cleared -- capture them here so who lifted the freeze,
|
|
15
|
+
# and what was being lifted, isn't lost. Idempotency key is keyed to this specific
|
|
16
|
+
# freeze (frozen_at), so a double-submitted unfreeze can't write two ledger rows, while
|
|
17
|
+
# a later freeze/unfreeze cycle on the same credit still gets its own row.
|
|
18
|
+
if credit.frozen_at.present?
|
|
19
|
+
write_ledger!(
|
|
20
|
+
store_credit: credit,
|
|
21
|
+
transaction_type: 'unfreeze',
|
|
22
|
+
amount: 0,
|
|
23
|
+
idempotency_key: "unfreeze:#{credit.id}:#{credit.frozen_at.to_i}",
|
|
24
|
+
admin: admin,
|
|
25
|
+
private_metadata: {
|
|
26
|
+
prior_frozen_by_id: credit.frozen_by_id,
|
|
27
|
+
prior_freeze_reason: credit.freeze_reason,
|
|
28
|
+
prior_freeze_level: credit.freeze_level
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
credit.update!(
|
|
34
|
+
frozen_at: nil,
|
|
35
|
+
frozen_by: nil,
|
|
36
|
+
freeze_reason: nil,
|
|
37
|
+
freeze_level: nil,
|
|
38
|
+
frozen_until: nil
|
|
39
|
+
)
|
|
40
|
+
credit
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
success(store_credit)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|