diff --git a/.env.sample b/.env.sample index bf66b2e28..29fb7727e 100644 --- a/.env.sample +++ b/.env.sample @@ -59,3 +59,13 @@ STOCKIT_API_TOKEN= SLACK_API_TOKEN= SLACK_PIN_CHANNEL= + +GOGOX_CLIENT_ID= +GOGOX_CLIENT_SECRET= +GOGOX_GRANT_TYPE= +GOGOX_HOST= +GOGOX_PAYMENT_METHOD= +CROSSROADS_GEOLOCATION= +CROSSROADS_STREET_ADDRESS= +CROSSROADS_CONTACT_NAME= +CROSSROADS_CONTACT_PHONE= diff --git a/Gemfile.lock b/Gemfile.lock index 1816bf2b1..da6f5f315 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: git@github.com:crossroads/go_go_van_api.git - revision: 3cbc5590e2a11a7903483de276bc668be88843cb + revision: e860edbdc14f67b02139cba678fd7d2a68f5f591 branch: master specs: go_go_van_api (0.0.1) diff --git a/app/controllers/ability.rb b/app/controllers/ability.rb index 8a4eecc1f..d0c7e5f61 100755 --- a/app/controllers/ability.rb +++ b/app/controllers/ability.rb @@ -24,7 +24,7 @@ class Ability can_manage_order_messages can_manage_offer_messages can_disable_user can_manage_stocktakes can_manage_stocktake_revisions can_manage_package_messages can_manage_organisations can_manage_user_roles - can_manage_canned_response + can_manage_canned_response can_manage_transport_orders ].freeze PERMISSION_NAMES.each do |permission_name| diff --git a/app/controllers/api/v1/ability.rb b/app/controllers/api/v1/ability.rb index 479579f10..9e629f880 100644 --- a/app/controllers/api/v1/ability.rb +++ b/app/controllers/api/v1/ability.rb @@ -6,6 +6,7 @@ def initialize(user) if user.present? @api_user = user.api_user? @user_offer_ids = user.offers.pluck(:id) + @user_order_ids = user.created_orders.pluck(:id) end super(user) @@ -55,6 +56,7 @@ def define_abilities offers_package_abilities canned_response_abilities processing_destination_abilities + transport_order_abilities end def processing_destination_abilities @@ -397,6 +399,16 @@ def schedule_abilities can [:index, :show], Schedule if can_read_schedule? end + def transport_order_abilities + can [:providers], TransportOrder + can [:quote, :book, :show, :cancel], TransportOrder, source_type: "Offer", source_id: @user_offer_ids + can [:quote, :book, :show, :cancel], TransportOrder, source_type: "Order", source_id: @user_order_ids + + if can_manage_transport_orders? + can [:show, :cancel, :quote, :book], TransportOrder + end + end + def location_abilities if (can_manage_locations? || @api_user) can %i[index create destroy], Location diff --git a/app/controllers/api/v1/transports_controller.rb b/app/controllers/api/v1/transports_controller.rb new file mode 100644 index 000000000..dcffab632 --- /dev/null +++ b/app/controllers/api/v1/transports_controller.rb @@ -0,0 +1,96 @@ +module Api + module V1 + class TransportsController < Api::V1::ApiController + + skip_authorization_check only: :update_hook + load_and_authorize_resource :transport_order, parent: false, except: [:update_hook] + + before_action :validate_transport_source, only: [:quote, :book] + + api :GET, '/v1/transports/providers', "List all GoodCity Tranports Options." + def providers + render json: TransportProvider.all.cached_json + end + + api :POST, '/v1/transports/quote', "Get provider quote" + param :provider, TRANSPORT_PROVIDERS, required: true, desc: "Provider selected for transport" + param :vehicle_type, String, required: true, desc: "Transport vehicle-type" + param :source_id, [Integer, String], required: true, desc: "Id of the source (offer/order)" + param :source_type, String, required: true, desc: "Type of the source (offer/order)" + param :schedule_at, String, desc: "Scheduled time for delivery" + param :district_id, String, desc: "Id of the district" + def quote + order_price = TransportService.new(transport_params.to_h).quotation + render json: order_price + end + + api :POST, '/v1/transports/book', "Book transport" + param :provider, TRANSPORT_PROVIDERS, required: true, desc: "Provider selected for transport" + param :vehicle_type, String, required: true, desc: "Transport vehicle-type" + param :source_id, String, required: true, desc: "Id of the source (offer/order)" + param :source_type, String, required: true, desc: "Type of the source (offer/order)" + param :schedule_at, String, required: true, desc: "Scheduled time for delivery" + param :district_id, String, desc: "Id of the district" + param :pickup_contact_name, String, desc: "Contact Person Name" + param :pickup_contact_phone, String, desc: "Contact Person Mobile" + param :pickup_street_address, String, required: true, desc: "Pickup Address" + def book + order_info = TransportService.new(transport_params.to_h).book + render json: order_info + end + + api :GET, '/v1/transports/:order_uuid', "Get GoodCity Tranport order details." + def show + order_info = TransportService.new({booking_id: params[:order_uuid], provider: transport_provider}).status + render json: order_info + end + + api :POST, '/v1/transports/:order_uuid/cancel', "Cancel GoodCity Tranport order." + def cancel + order_info = TransportService.new({booking_id: params[:order_uuid], provider: transport_provider}).cancel + render json: order_info + end + + api :POST, '/v1/transports/update_hook', "Webhook to update transport status" + def update_hook + # setup ngrok and inspect response + # response details are not yet available from Gogox Provider + end + + private + + def validate_transport_source + if params['source_type'] == 'Offer' + if !User.current_user.offers.pluck(:id).include?(params['source_id'].to_i) + raise Goodcity::UnauthorizedError.with_text("You are not authorized to book transport for this offer.") + end + end + + if params['source_type'] == 'Order' + if !User.current_user.created_orders.pluck(:id).include?(params['source_id'].to_i) + raise Goodcity::UnauthorizedError.with_text("You are not authorized to book transport for this order.") + end + end + end + + def transport_provider + order = TransportOrder.find_by(order_uuid: params[:order_uuid]) + order.try(:transport_provider).try(:name) + end + + def transport_params + set_district_id unless params["district_id"].presence + params.permit([ + "schedule_at", "district_id", "provider", "vehicle_type", + "pickup_street_address", "pickup_contact_name", "pickup_contact_phone", + "source_type", "source_id" + ]) + end + + def set_district_id + params["district_id"] = User.current_user.address.district_id + end + + end + end +end diff --git a/app/models/offer.rb b/app/models/offer.rb index 205072916..814132470 100755 --- a/app/models/offer.rb +++ b/app/models/offer.rb @@ -31,6 +31,7 @@ class Offer < ApplicationRecord has_many :missing_packages, class_name: 'Package', through: :items, source: :missing_packages has_many :received_packages, class_name: 'Package', through: :items, source: :received_packages has_one :delivery, dependent: :destroy + has_one :transport_order, as: :source, dependent: :destroy has_many :users, through: :subscriptions, source: :subscribable, source_type: 'Offer' has_many :offers_packages has_many :packages, through: :offers_packages diff --git a/app/models/transport_order.rb b/app/models/transport_order.rb new file mode 100644 index 000000000..9bad56afb --- /dev/null +++ b/app/models/transport_order.rb @@ -0,0 +1,4 @@ +class TransportOrder < ApplicationRecord + belongs_to :transport_provider + belongs_to :source, polymorphic: true +end diff --git a/app/models/transport_provider.rb b/app/models/transport_provider.rb new file mode 100644 index 000000000..fb3d67f51 --- /dev/null +++ b/app/models/transport_provider.rb @@ -0,0 +1,3 @@ +class TransportProvider < ApplicationRecord + include CacheableJson +end diff --git a/app/serializers/api/v1/transport_provider_serializer.rb b/app/serializers/api/v1/transport_provider_serializer.rb new file mode 100644 index 000000000..3a8e27cd5 --- /dev/null +++ b/app/serializers/api/v1/transport_provider_serializer.rb @@ -0,0 +1,5 @@ +module Api::V1 + class TransportProviderSerializer < ApplicationSerializer + attributes :id, :name, :logo, :description, :metadata + end +end diff --git a/app/services/gogox.rb b/app/services/gogox.rb new file mode 100644 index 000000000..ace3583da --- /dev/null +++ b/app/services/gogox.rb @@ -0,0 +1,128 @@ +class Gogox + + attr_accessor :params, :time, :vehicle + + VEHICLE_TYPES = ["van", "mudou", "mudou9"] + + def initialize(options = {}) + @params = options + @time = parse_pickup_time(options[:schedule_at]) + @vehicle = options[:vehicle_type] + end + + # Rsponse + # { + # "uuid" => "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + # "status" => "pending", + # "vehicle_type" => "van", + # "payment_method" => "prepaid_wallet", + # "courier" => {}, + # "pickup" => { + # "name" => "Swati J", + # "street_address" => "123", + # "floor_or_unit_number" => nil, + # "schedule_at" => 1609242940, + # "location" => {"lat" => 22.5029632, "lng" => 114.1277213}, + # "contact" => { + # "name" => "Swati J", + # "phone_number" => "51111113", + # "phone_extension" => nil + # } + # }, + # "destinations" => [{ + # "name" => "GCAdmin User", + # "street_address" => "Santa Peak Road", + # "floor_or_unit_number" => nil, + # "location" => {"lat" => 32.3700365, "lng" => 120.9930016}, + # "contact" => { + # "name" => "GCAdmin User", + # "phone_number" => "51111111" + # } + # }], + # "note_to_courier" => nil, + # "price" => {"amount" => 15000, "currency" => "HKD"}, + # "price_breakdown" => [{"key" => "fee", "amount" => 15000}] + # } + def book + GogoxApi::Transport.new(order_attributes).order + end + + # Response: + # { + # "vehicle_type" => "van", + # "estimated_price" => {"amount" => 15000, "currency" => "HKD"}, + # "estimated_price_breakdown" => [{"key" => "fee", "amount" => 15000}] + # } + def quotation + GogoxApi::Transport.new(quotation_attributes).quotation + end + + class << self + + def transport_status(booking_id) + GogoxApi::Transport.new.status(booking_id) + end + + # Response + # Response is nil on successful cancellation of GOGOX transport + def cancel_order(booking_id) + response = GogoxApi::Transport.new.cancel(booking_id) + if !response + { + order_uuid: booking_id, + status: "cancelled" + } + end + end + + end + + private + + def order_attributes + { + 'vehicle_type': vehicle_type, + "pickup_location": params[:pickup_location], + "pickup_street_address": params[:pickup_street_address], + "schedule_at": parse_time, + "pickup_contact_name": params[:pickup_contact_name], + "pickup_contact_phone": params[:pickup_contact_phone], + "destination_location": params[:destination_location], + "destination_street_address": params[:destination_street_address], + "destination_contact_name": params[:destination_contact_name], + "destination_contact_phone": params[:destination_contact_phone] + } + end + + def quotation_attributes + { + 'vehicle_type': vehicle_type, + "schedule_at": parse_time, + "pickup_location": params[:pickup_location], + "destination_location": params[:destination_location] + } + end + + def vehicle_type + if vehicle.blank? || !VEHICLE_TYPES.include?(vehicle) + raise(ValueError, "vehicle should be from #{VEHICLE_TYPES.join(', ')}") + end + vehicle + end + + def parse_pickup_time(time = nil) + return time if time.present? + + # next available date within next 5 days + next_available_date = DateSet.new(5, 1).available_dates.first + (next_available_date.beginning_of_day + 12.hours) + end + + def parse_time + @time = DateTime.parse(@time.to_s) + @time = @time.zone == "HKT" ? @time : @time.in_time_zone("Asia/Hong_Kong") + @time.to_i + end + + class ValueError < StandardError; end +end diff --git a/app/services/transport_service.rb b/app/services/transport_service.rb new file mode 100644 index 000000000..711da6509 --- /dev/null +++ b/app/services/transport_service.rb @@ -0,0 +1,107 @@ +class TransportService + + attr_accessor :provider_name, :params, :provider, :booking_id, :user, :district_id + + def initialize(options={}) + @params = options + @provider_name = ([options.dig(:provider)] & TRANSPORT_PROVIDERS)[0] + @provider ||= Object::const_get(provider_name) + @booking_id = options.dig(:booking_id) + @transport_constants = Rails.application.secrets.transport + + fetch_user + fetch_district_id + end + + def quotation + @provider.new(quotation_attributes).quotation + end + + def book + response = @provider.new(order_attributes).book + if response[:error] + response[:error] + else + store_order_details(response) + end + end + + def cancel + response = @provider.cancel_order(booking_id) + if response + update_order_details({ + status: "cancelled", + order_uuid: booking_id + }) + end + end + + def status + response = @provider.transport_status(booking_id) + if response + update_order_details({ + status: response["status"], + order_uuid: booking_id, + metadata: response + }) + end + end + + private + + def store_order_details(response) + TransportOrder.create( + transport_provider_id: TransportProvider.find_by(name: provider_name.upcase).try(:id), + order_uuid: response["uuid"], + status: response["status"], + scheduled_at: Time.at(response["pickup"]["schedule_at"]).in_time_zone, + metadata: response, + source_id: @params[:source_id], + source_type: @params[:source_type] + ) + end + + def update_order_details(response) + order = TransportOrder.find_by(order_uuid: response[:order_uuid]) + order.update_attributes(response) + order + end + + def quotation_attributes + { + 'vehicle_type': params[:vehicle_type], + "schedule_at": params[:schedule_at], + "pickup_location": pickup_location, + "destination_location": @transport_constants[:crossroads_geolocation] + } + end + + def pickup_location + pickup_district = District.find(@district_id) + [pickup_district.latitude, pickup_district.longitude] + end + + def order_attributes + { + 'vehicle_type': params[:vehicle_type], + "pickup_location": pickup_location, + "pickup_street_address": params[:pickup_street_address], + "schedule_at": params[:schedule_at], + "pickup_contact_name": params[:pickup_contact_name] || @user.full_name, + "pickup_contact_phone": params[:pickup_contact_phone] || @user.mobile, + "destination_location": @transport_constants[:crossroads_geolocation], + "destination_street_address": @transport_constants[:crossroads_street_address], + "destination_contact_name": @transport_constants[:crossroads_contact_name], + "destination_contact_phone": @transport_constants[:crossroads_contact_phone] + } + end + + def fetch_user + @user ||= User.current_user + end + + def fetch_district_id + @district_id ||= @params[:district_id].presence || @user.address.district_id + end + +end diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index e55bd90d1..ee50a8c22 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -33,3 +33,4 @@ DEFAULT_COUNTRY = 'China - Hong Kong (Special Administrative Region)' GOODCITY_FROM_EMAIL = 'GoodCity ' GOODCITY_ORDER_FROM_EMAIL = 'GoodCity ' +TRANSPORT_PROVIDERS = ['Gogox'] diff --git a/config/routes.rb b/config/routes.rb index 800965672..a3c1f9990 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,7 +16,7 @@ resources :users do get :me, on: :collection end - + resources :shareables, only: [:show, :index, :create, :destroy, :update] do collection do delete :unshare @@ -215,6 +215,13 @@ resources :printers_users, only: [:create, :update] resources :processing_destinations, only: :index + get "transports/providers", to: "transports#providers" + post "transports/quote", to: "transports#quote" + post "transports/book", to: "transports#book" + get "transports/:order_uuid", to: "transports#show" + post "transports/order_uuid/cancel", to: "transports#cancel" + post "transports/update_hook", to: "transports#update_hook" + # routes used in stock app get "designations", to: "orders#index" get "designations/:id", to: "orders#show" diff --git a/config/secrets.yml b/config/secrets.yml index 477844e1b..ed6bb1243 100755 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -55,6 +55,11 @@ base: &BASE printer_host: <%=ENV['BARCODE_PRINTER_HOST']%> printer_user: <%=ENV['BARCODE_PRINTER_USER']%> printer_pwd: <%=ENV['BARCODE_PRINTER_PWD']%> + transport: + crossroads_geolocation: <%=ENV['CROSSROADS_GEOLOCATION']%> + crossroads_street_address: <%=ENV['CROSSROADS_STREET_ADDRESS']%> + crossroads_contact_name: <%=ENV['CROSSROADS_CONTACT_NAME']%> + crossroads_contact_phone: <%=ENV['CROSSROADS_CONTACT_PHONE']%> development: <<: *BASE diff --git a/db/migrate/20201210092143_create_transport_providers.rb b/db/migrate/20201210092143_create_transport_providers.rb new file mode 100644 index 000000000..2d9f281b4 --- /dev/null +++ b/db/migrate/20201210092143_create_transport_providers.rb @@ -0,0 +1,12 @@ +class CreateTransportProviders < ActiveRecord::Migration[5.2] + def change + create_table :transport_providers do |t| + t.string :name + t.string :logo + t.text :description + t.jsonb :metadata, default: '{}' + + t.timestamps + end + end +end diff --git a/db/migrate/20210111115250_create_transport_orders.rb b/db/migrate/20210111115250_create_transport_orders.rb new file mode 100644 index 000000000..967ef49cb --- /dev/null +++ b/db/migrate/20210111115250_create_transport_orders.rb @@ -0,0 +1,15 @@ +class CreateTransportOrders < ActiveRecord::Migration[5.2] + def change + create_table :transport_orders do |t| + t.integer :transport_provider_id + t.string :order_uuid + t.string :status + t.datetime :scheduled_at + t.jsonb :metadata + t.integer :source_id + t.string :source_type + + t.timestamps + end + end +end diff --git a/db/permissions_roles.yml b/db/permissions_roles.yml index 1713204f2..09416e027 100644 --- a/db/permissions_roles.yml +++ b/db/permissions_roles.yml @@ -32,6 +32,7 @@ Reviewer: - can_update_my_printers - can_read_or_modify_user - can_manage_canned_response +- can_manage_transport_orders Supervisor: - can_manage_packages @@ -62,6 +63,7 @@ Supervisor: - can_manage_user_roles - can_manage_printers - can_manage_canned_response +- can_manage_transport_orders Stock fulfilment: &stock_fulfilment - can_manage_packages diff --git a/db/schema.rb b/db/schema.rb index 6d3ff3d48..f92ed63d7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,18 +13,19 @@ ActiveRecord::Schema.define(version: 2021_01_18_111336) do # These are extensions that must be enabled in order to support this database - enable_extension "fuzzystrmatch" + enable_extension "btree_gin" enable_extension "pg_trgm" + enable_extension "pgcrypto" enable_extension "plpgsql" create_table "addresses", id: :serial, force: :cascade do |t| - t.string "flat", limit: 255 - t.string "building", limit: 255 - t.string "street", limit: 255 + t.string "flat" + t.string "building" + t.string "street" t.integer "district_id" t.integer "addressable_id" - t.string "addressable_type", limit: 255 - t.string "address_type", limit: 255 + t.string "addressable_type" + t.string "address_type" t.datetime "created_at" t.datetime "updated_at" t.datetime "deleted_at" @@ -47,7 +48,7 @@ create_table "auth_tokens", id: :serial, force: :cascade do |t| t.datetime "otp_code_expiry" - t.string "otp_secret_key", limit: 255 + t.string "otp_secret_key" t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" @@ -75,7 +76,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "identifier" - t.index ["identifier"], name: "index_booking_types_on_identifier" + t.index ["name_en", "name_zh_tw"], name: "index_booking_types_on_name_en_and_name_zh_tw", unique: true end create_table "boxes", id: :serial, force: :cascade do |t| @@ -159,8 +160,8 @@ end create_table "contacts", id: :serial, force: :cascade do |t| - t.string "name", limit: 255 - t.string "mobile", limit: 255 + t.string "name" + t.string "mobile" t.datetime "created_at" t.datetime "updated_at" t.datetime "deleted_at" @@ -174,8 +175,8 @@ end create_table "crossroads_transports", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.datetime "created_at" t.datetime "updated_at" t.integer "cost" @@ -187,7 +188,7 @@ t.integer "offer_id" t.integer "contact_id" t.integer "schedule_id" - t.string "delivery_type", limit: 255 + t.string "delivery_type" t.datetime "start" t.datetime "finish" t.datetime "created_at" @@ -201,8 +202,8 @@ end create_table "districts", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.integer "territory_id" t.datetime "created_at" t.datetime "updated_at" @@ -212,8 +213,8 @@ end create_table "donor_conditions", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.datetime "created_at" t.datetime "updated_at" t.boolean "visible_to_donor", default: true, null: false @@ -238,7 +239,7 @@ create_table "gogovan_orders", id: :serial, force: :cascade do |t| t.integer "booking_id" - t.string "status", limit: 255 + t.string "status" t.datetime "created_at" t.datetime "updated_at" t.datetime "deleted_at" @@ -252,8 +253,8 @@ end create_table "gogovan_transports", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.datetime "created_at" t.datetime "updated_at" t.boolean "disabled", default: false @@ -282,7 +283,7 @@ create_table "holidays", id: :serial, force: :cascade do |t| t.datetime "holiday" t.integer "year" - t.string "name", limit: 255 + t.string "name" t.datetime "created_at" t.datetime "updated_at" end @@ -296,7 +297,7 @@ end create_table "images", id: :serial, force: :cascade do |t| - t.string "cloudinary_id", limit: 255 + t.string "cloudinary_id" t.boolean "favourite", default: false t.integer "item_id" t.datetime "created_at" @@ -314,11 +315,11 @@ create_table "items", id: :serial, force: :cascade do |t| t.text "donor_description" - t.string "state", limit: 255 + t.string "state" t.integer "offer_id", null: false t.integer "package_type_id" t.integer "rejection_reason_id" - t.string "reject_reason", limit: 255 + t.string "reject_reason" t.datetime "created_at" t.datetime "updated_at" t.integer "donor_condition_id" @@ -335,6 +336,8 @@ t.string "area" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["area"], name: "index_locations_on_area", using: :gin + t.index ["building"], name: "index_locations_on_building", using: :gin end create_table "lookups", id: :serial, force: :cascade do |t| @@ -370,17 +373,18 @@ t.integer "messageable_id" t.jsonb "lookup", default: {} t.integer "recipient_id" + t.index ["body"], name: "messages_body_search_idx", using: :gin t.index ["lookup"], name: "index_messages_on_lookup", using: :gin t.index ["sender_id"], name: "index_messages_on_sender_id" end create_table "offers", id: :serial, force: :cascade do |t| - t.string "language", limit: 255 - t.string "state", limit: 255 - t.string "origin", limit: 255 + t.string "language" + t.string "state" + t.string "origin" t.boolean "stairs" t.boolean "parking" - t.string "estimated_size", limit: 255 + t.string "estimated_size" t.text "notes" t.integer "created_by_id" t.datetime "created_at" @@ -409,6 +413,7 @@ t.index ["created_by_id"], name: "index_offers_on_created_by_id" t.index ["crossroads_transport_id"], name: "index_offers_on_crossroads_transport_id" t.index ["gogovan_transport_id"], name: "index_offers_on_gogovan_transport_id" + t.index ["notes"], name: "offers_notes_search_idx", using: :gin t.index ["received_by_id"], name: "index_offers_on_received_by_id" t.index ["reviewed_by_id"], name: "index_offers_on_reviewed_by_id" t.index ["state"], name: "index_offers_on_state" @@ -445,12 +450,9 @@ t.string "code" t.string "detail_type" t.integer "detail_id" - t.integer "stockit_contact_id" - t.integer "stockit_organisation_id" t.datetime "created_at" t.datetime "updated_at", null: false t.text "description" - t.integer "stockit_activity_id" t.integer "country_id" t.integer "created_by_id" t.integer "processed_by_id" @@ -478,10 +480,14 @@ t.integer "cancellation_reason_id" t.boolean "continuous", default: false t.date "shipment_date" + t.integer "stockit_activity_id" + t.integer "stockit_organisation_id" + t.integer "stockit_contact_id" t.index ["address_id"], name: "index_orders_on_address_id" t.index ["beneficiary_id"], name: "index_orders_on_beneficiary_id" t.index ["cancelled_by_id"], name: "index_orders_on_cancelled_by_id" t.index ["closed_by_id"], name: "index_orders_on_closed_by_id" + t.index ["code"], name: "orders_code_idx", using: :gin t.index ["country_id"], name: "index_orders_on_country_id" t.index ["created_by_id"], name: "index_orders_on_created_by_id" t.index ["detail_id", "detail_type"], name: "index_orders_on_detail_id_and_detail_type" @@ -492,9 +498,6 @@ t.index ["processed_by_id"], name: "index_orders_on_processed_by_id" t.index ["shipment_date"], name: "index_orders_on_shipment_date" t.index ["state"], name: "index_orders_on_state" - t.index ["stockit_activity_id"], name: "index_orders_on_stockit_activity_id" - t.index ["stockit_contact_id"], name: "index_orders_on_stockit_contact_id" - t.index ["stockit_organisation_id"], name: "index_orders_on_stockit_organisation_id" t.index ["submitted_by_id"], name: "index_orders_on_submitted_by_id" end @@ -608,23 +611,23 @@ t.boolean "visible_in_selects", default: false t.integer "location_id" t.boolean "allow_requests", default: true - t.boolean "allow_pieces", default: false t.boolean "allow_stock", default: false + t.boolean "allow_pieces", default: false t.string "subform" t.boolean "allow_box", default: false t.boolean "allow_pallet", default: false - t.decimal "default_value_hk_dollar" t.boolean "allow_expiry_date", default: false - t.text "description_en" - t.text "description_zh_tw" + t.decimal "default_value_hk_dollar" t.integer "length" t.integer "width" t.integer "height" t.string "department" + t.text "description_en" + t.text "description_zh_tw" t.index ["allow_requests"], name: "index_package_types_on_allow_requests" - t.index ["description_en"], name: "index_package_types_on_description_en" - t.index ["description_zh_tw"], name: "index_package_types_on_description_zh_tw" t.index ["location_id"], name: "index_package_types_on_location_id" + t.index ["name_en"], name: "package_types_name_en_search_idx", using: :gin + t.index ["name_zh_tw"], name: "package_types_name_zh_tw_search_idx", using: :gin t.index ["visible_in_selects"], name: "index_package_types_on_visible_in_selects" end @@ -634,7 +637,7 @@ t.integer "height" t.text "notes", null: false t.integer "item_id" - t.string "state", limit: 255 + t.string "state" t.datetime "received_at" t.datetime "rejected_at" t.integer "package_type_id" @@ -650,13 +653,9 @@ t.integer "box_id" t.integer "pallet_id" t.integer "order_id" - t.date "stockit_sent_on" - t.date "stockit_designated_on" - t.integer "stockit_designated_by_id" - t.integer "stockit_sent_by_id" + t.integer "on_hand_boxed_quantity", default: 0 + t.integer "on_hand_palletized_quantity", default: 0 t.integer "favourite_image_id" - t.date "stockit_moved_on" - t.integer "stockit_moved_by_id" t.boolean "saleable" t.string "case_number" t.boolean "allow_web_publish" @@ -666,37 +665,43 @@ t.integer "detail_id" t.string "detail_type" t.integer "storage_type_id" - t.decimal "value_hk_dollar" t.integer "available_quantity", default: 0 t.integer "on_hand_quantity", default: 0 t.integer "designated_quantity", default: 0 t.integer "dispatched_quantity", default: 0 t.date "expiry_date" + t.decimal "value_hk_dollar" t.integer "package_set_id" t.integer "restriction_id" t.text "comment" - t.integer "on_hand_boxed_quantity", default: 0 - t.integer "on_hand_palletized_quantity", default: 0 + t.integer "stockit_moved_by_id" + t.datetime "stockit_moved_on" + t.integer "stockit_sent_by_id" + t.integer "stockit_designated_by_id" + t.datetime "stockit_designated_on" + t.datetime "stockit_sent_on" t.text "notes_zh_tw" t.index ["allow_web_publish"], name: "index_packages_on_allow_web_publish" t.index ["available_quantity"], name: "index_packages_on_available_quantity" t.index ["box_id"], name: "index_packages_on_box_id" + t.index ["case_number"], name: "index_packages_on_case_number", using: :gin t.index ["designated_quantity"], name: "index_packages_on_designated_quantity" + t.index ["designation_name"], name: "index_packages_on_designation_name", using: :gin t.index ["detail_type", "detail_id"], name: "index_packages_on_detail_type_and_detail_id" t.index ["dispatched_quantity"], name: "index_packages_on_dispatched_quantity" t.index ["donor_condition_id"], name: "index_packages_on_donor_condition_id" t.index ["inventory_number"], name: "index_packages_on_inventory_number" + t.index ["inventory_number"], name: "inventory_numbers_search_idx", using: :gin t.index ["item_id"], name: "index_packages_on_item_id" t.index ["location_id"], name: "index_packages_on_location_id" + t.index ["notes"], name: "index_packages_on_notes", using: :gin t.index ["offer_id"], name: "index_packages_on_offer_id" t.index ["on_hand_quantity"], name: "index_packages_on_on_hand_quantity" t.index ["order_id"], name: "index_packages_on_order_id" t.index ["package_set_id"], name: "index_packages_on_package_set_id" t.index ["package_type_id"], name: "index_packages_on_package_type_id" t.index ["pallet_id"], name: "index_packages_on_pallet_id" - t.index ["stockit_designated_by_id"], name: "index_packages_on_stockit_designated_by_id" - t.index ["stockit_moved_by_id"], name: "index_packages_on_stockit_moved_by_id" - t.index ["stockit_sent_by_id"], name: "index_packages_on_stockit_sent_by_id" + t.index ["state"], name: "index_packages_on_state", using: :gin t.index ["storage_type_id"], name: "index_packages_on_storage_type_id" end @@ -747,7 +752,7 @@ end create_table "permissions", id: :serial, force: :cascade do |t| - t.string "name", limit: 255 + t.string "name" t.datetime "created_at" t.datetime "updated_at" end @@ -792,10 +797,10 @@ end create_table "rejection_reasons", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 + t.string "name_en" t.datetime "created_at" t.datetime "updated_at" - t.string "name_zh_tw", limit: 255 + t.string "name_zh_tw" end create_table "requested_packages", id: :serial, force: :cascade do |t| @@ -834,10 +839,10 @@ end create_table "schedules", id: :serial, force: :cascade do |t| - t.string "resource", limit: 255 + t.string "resource" t.integer "slot" - t.string "slot_name", limit: 255 - t.string "zone", limit: 255 + t.string "slot_name" + t.string "zone" t.datetime "scheduled_at" t.datetime "created_at" t.datetime "updated_at" @@ -877,6 +882,10 @@ t.integer "stockit_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["first_name"], name: "st_contacts_first_name_idx", using: :gin + t.index ["last_name"], name: "st_contacts_last_name_idx", using: :gin + t.index ["mobile_phone_number"], name: "st_contacts_mobile_phone_number_idx", using: :gin + t.index ["phone_number"], name: "st_contacts_phone_number_idx", using: :gin end create_table "stockit_local_orders", id: :serial, force: :cascade do |t| @@ -887,6 +896,7 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "purpose_of_goods" + t.index ["client_name"], name: "st_local_orders_client_name_idx", using: :gin end create_table "stockit_organisations", id: :serial, force: :cascade do |t| @@ -894,6 +904,7 @@ t.integer "stockit_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["name"], name: "st_organisations_name_idx", using: :gin end create_table "stocktake_revisions", id: :serial, force: :cascade do |t| @@ -942,15 +953,15 @@ t.boolean "is_default", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["package_type_id", "package_type_id"], name: "index_subpackage_types_on_package_type_id_and_package_type_id" t.index ["package_type_id"], name: "index_subpackage_types_on_package_type_id" + t.index ["package_type_id"], name: "index_subpackage_types_on_package_type_id_and_package_type_id" t.index ["subpackage_type_id"], name: "index_subpackage_types_on_subpackage_type_id" end create_table "subscriptions", id: :serial, force: :cascade do |t| t.integer "user_id" t.integer "message_id" - t.string "state", limit: 255 + t.string "state" t.string "subscribable_type" t.integer "subscribable_id" t.index ["message_id"], name: "index_subscriptions_on_message_id" @@ -959,19 +970,40 @@ end create_table "territories", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.datetime "created_at" t.datetime "updated_at" end create_table "timeslots", id: :serial, force: :cascade do |t| - t.string "name_en", limit: 255 - t.string "name_zh_tw", limit: 255 + t.string "name_en" + t.string "name_zh_tw" t.datetime "created_at" t.datetime "updated_at" end + create_table "transport_orders", force: :cascade do |t| + t.integer "transport_provider_id" + t.string "order_uuid" + t.string "status" + t.datetime "scheduled_at" + t.jsonb "metadata" + t.integer "source_id" + t.string "source_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "transport_providers", force: :cascade do |t| + t.string "name" + t.string "logo" + t.text "description" + t.jsonb "metadata", default: "{}" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "user_favourites", force: :cascade do |t| t.string "favourite_type" t.integer "favourite_id" @@ -995,9 +1027,9 @@ end create_table "users", id: :serial, force: :cascade do |t| - t.string "first_name", limit: 255 - t.string "last_name", limit: 255 - t.string "mobile", limit: 255 + t.string "first_name" + t.string "last_name" + t.string "mobile" t.datetime "created_at" t.datetime "updated_at" t.integer "image_id" @@ -1094,10 +1126,7 @@ add_foreign_key "orders", "countries", name: "orders_country_id_fk" add_foreign_key "orders", "districts", name: "orders_district_id_fk" add_foreign_key "orders", "organisations", name: "orders_organisation_id_fk" - add_foreign_key "orders", "stockit_activities", name: "orders_stockit_activity_id_fk" - add_foreign_key "orders", "stockit_contacts", name: "orders_stockit_contact_id_fk" add_foreign_key "orders", "stockit_local_orders", column: "detail_id", name: "orders_detail_id_fk" - add_foreign_key "orders", "stockit_organisations", name: "orders_stockit_organisation_id_fk" add_foreign_key "orders", "users", column: "cancelled_by_id", name: "orders_cancelled_by_id_fk" add_foreign_key "orders", "users", column: "closed_by_id", name: "orders_closed_by_id_fk" add_foreign_key "orders", "users", column: "created_by_id", name: "orders_created_by_id_fk" @@ -1131,9 +1160,6 @@ add_foreign_key "packages", "pallets", name: "packages_pallet_id_fk" add_foreign_key "packages", "restrictions", name: "packages_restriction_id_fk" add_foreign_key "packages", "storage_types", name: "packages_storage_type_id_fk" - add_foreign_key "packages", "users", column: "stockit_designated_by_id", name: "packages_stockit_designated_by_id_fk" - add_foreign_key "packages", "users", column: "stockit_moved_by_id", name: "packages_stockit_moved_by_id_fk" - add_foreign_key "packages", "users", column: "stockit_sent_by_id", name: "packages_stockit_sent_by_id_fk" add_foreign_key "packages_inventories", "locations" add_foreign_key "packages_inventories", "packages" add_foreign_key "packages_inventories", "users" diff --git a/db/transport_providers.yml b/db/transport_providers.yml new file mode 100644 index 000000000..e74722393 --- /dev/null +++ b/db/transport_providers.yml @@ -0,0 +1,20 @@ +--- +- + name: "GOGOX" + logo: "https://s3-ap-northeast-1.amazonaws.com/wp-gogovan.com/wp-content/uploads/sites/5/2019/09/19111727/GOGOX_logo_white_24px.png" + description: "A 24/7 van hailing platform." + metadata: { + transport_types: [ + "van", + "truck" + ], + supports_quotes: true, + includes_road_fees: true, + provides_helping_hand: false, + working_hours: [ + "00:00", + "23:59" + ] + } + + diff --git a/spec/controllers/api/v1/transports_controller_spec.rb b/spec/controllers/api/v1/transports_controller_spec.rb new file mode 100644 index 000000000..f8fe34f58 --- /dev/null +++ b/spec/controllers/api/v1/transports_controller_spec.rb @@ -0,0 +1,155 @@ +require 'rails_helper' + +RSpec.describe Api::V1::TransportsController, type: :controller do + + let(:user) { create(:user, :with_token) } + let(:district) { create :district } + let(:offer) { create :offer, created_by_id: user.id } + let(:scheduled_at) { "Wed, 30 Dec 2020 15:10:08.000000000 +0800" } + let!(:provider) { create(:transport_provider) } + + describe "GET providers" do + before { generate_and_set_token(user) } + + it "returns 200", :show_in_doc do + get :providers + expect(response.status).to eq(200) + end + + it "return providers" do + get :providers + body = JSON.parse(response.body) + expect( body["transport_providers"].length ).to eq(TransportProvider.count) + end + end + + describe "POST quote" do + before { generate_and_set_token(user) } + + let(:quote_response) { + { + "vehicle_type" => "van", + "estimated_price" => {"amount" => 15000, "currency" => "HKD"}, + "estimated_price_breakdown" => [{"key" => "fee", "amount" => 15000}], + } + } + + let(:quotation_attributes) { + { + "provider": "Gogox", + 'vehicle_type': 'van', + "schedule_at": scheduled_at, + "district_id": district.id.to_s, + "source_id": offer.id.to_s, + "source_type": 'Offer' + } + } + + it "should trigger TransportService Service" do + mock_object = instance_double(TransportService, quotation: quote_response) + allow(TransportService).to receive(:new).with(quotation_attributes) + .and_return(mock_object) + + post :quote, params: quotation_attributes + + expect( JSON.parse(response.body) ).to eq(quote_response) + end + end + + describe "POST book" do + before { generate_and_set_token(user) } + + let(:order_response) { + { + "vehicle_type" => "van", + "price" => {"amount" => 15000, "currency" => "HKD"}, + "price_breakdown" => [{"key" => "fee", "amount" => 15000}], + } + } + + let(:order_attributes) { + { + "provider": "Gogox", + 'vehicle_type': "van", + "schedule_at": scheduled_at, + "district_id": district.id.to_s, + "source_id": offer.id.to_s, + "source_type": "Offer", + "pickup_contact_name": "Sarah", + "pickup_contact_phone": "+85251111117", + "pickup_street_address": "Street" + } + } + + it "should trigger TransportService Service" do + mock_object = instance_double(TransportService, book: order_response) + allow(TransportService).to receive(:new).with(order_attributes) + .and_return(mock_object) + + post :book, params: order_attributes + + expect(JSON.parse(response.body)).to eq(order_response) + end + end + + describe "GET show" do + before { generate_and_set_token(user) } + + let(:booking_id) { "2f859363-5c43-4fe2-9b91-6c6c43d610d2" } + let!(:transport_order) { + create(:transport_order, order_uuid: booking_id, transport_provider: provider) + } + + let(:status_response) { + transport_order.attributes.to_json + } + + let(:status_attributes) { + { + booking_id: booking_id, + provider: "GOGOX" + } + } + + it "should trigger TransportService Service" do + mock_object = instance_double(TransportService, status: status_response) + allow(TransportService).to receive(:new).with(status_attributes) + .and_return(mock_object) + + get :show, params: {order_uuid: booking_id} + + expect( JSON.parse(response.body) ).to eq(JSON.parse(status_response)) + end + end + + describe "POST cancel" do + before { generate_and_set_token(user) } + + let(:booking_id) { "2f859363-5c43-4fe2-9b91-6c6c43d610d2" } + let!(:transport_order) { + create(:transport_order, order_uuid: booking_id, transport_provider: provider) + } + + let(:cancel_response) { + transport_order.attributes.to_json + } + + let(:cancel_attributes) { + { + booking_id: booking_id, + provider: "GOGOX" + } + } + + it "should trigger TransportService Service" do + mock_object = instance_double(TransportService, cancel: cancel_response) + allow(TransportService).to receive(:new).with(cancel_attributes) + .and_return(mock_object) + + post :cancel, params: {order_uuid: booking_id} + + expect( JSON.parse(response.body) ).to eq(JSON.parse(cancel_response)) + end + end + +end diff --git a/spec/factories/transport_orders.rb b/spec/factories/transport_orders.rb new file mode 100644 index 000000000..bf35f96ab --- /dev/null +++ b/spec/factories/transport_orders.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :transport_order do + transport_provider_id { 1 } + order_uuid { "MyString" } + status { "MyString" } + scheduled_at { "2021-01-11 17:22:50" } + metadata { "" } + source_id { 1 } + source_type { 'Offer' } + end +end diff --git a/spec/factories/transport_providers.rb b/spec/factories/transport_providers.rb new file mode 100644 index 000000000..b4e7c5316 --- /dev/null +++ b/spec/factories/transport_providers.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :transport_provider do + name { "GOGOX" } + logo { "example.png" } + description { "A 24/7 van hailing platform." } + metadata { { } } + end +end diff --git a/spec/models/transport_order_spec.rb b/spec/models/transport_order_spec.rb new file mode 100644 index 000000000..ace5a562e --- /dev/null +++ b/spec/models/transport_order_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TransportOrder, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/transport_provider_spec.rb b/spec/models/transport_provider_spec.rb new file mode 100644 index 000000000..20818ebee --- /dev/null +++ b/spec/models/transport_provider_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe TransportProvider, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/gogox_spec.rb b/spec/services/gogox_spec.rb new file mode 100644 index 000000000..491404775 --- /dev/null +++ b/spec/services/gogox_spec.rb @@ -0,0 +1,192 @@ +require "rails_helper" + +describe Gogox do + + let(:schedule_at) { "Wed, 30 Dec 2020 15:10:08.000000000 +0800" } + let(:district) { (create :district) } + let(:vehicle) { "van" } + let(:params) { + { + 'district_id' => district.id, + } + } + + let(:gogovan) { Gogox.new(attributes) } + + let(:attributes) { + { + district_id: district.id, + vehicle_type: vehicle, + schedule_at: schedule_at, + params: params, + destination_location: [56.8766894, 120.9891416], + pickup_location: [22.5029632, 114.1277213], + destination_contact_name: "Admin User", + destination_contact_phone: "+85251111111", + destination_street_address: "Castle Peak Rd (So Kwun Wat)", + } + } + + context "initialization" do + it "time" do + expect(gogovan.time).to eql(schedule_at) + end + it "vehicle" do + expect(gogovan.vehicle).to eql(vehicle) + end + end + + context "quotation" do + let(:response) { + { + "vehicle_type" => "van", + "estimated_price" => {"amount" => 15000, "currency" => "HKD"}, + "estimated_price_breakdown" => [{"key" => "fee", "amount" => 15000}] + } + } + + let(:quotation_attributes) { + { + 'vehicle_type': vehicle, + "schedule_at": 1609312208, + "pickup_location": [22.5029632, 114.1277213], + "destination_location": [56.8766894, 120.9891416] + } + } + + it 'should trigger Gogox API' do + mock_object = instance_double(GogoxApi::Transport, quotation: response) + allow(GogoxApi::Transport).to receive(:new).with(quotation_attributes) + .and_return(mock_object) + expect(gogovan.quotation).to eq(response) + end + end + + context "book" do + let(:gogovan) { + Gogox.new({ + district_id: district.id, + vehicle_type: vehicle, + schedule_at: schedule_at, + params: params, + pickup_contact_name: "David", + pickup_contact_phone: "+85251111112", + pickup_street_address: "Street", + destination_location: [56.8766894, 120.9891416], + pickup_location: [22.5029632, 114.1277213], + destination_contact_name: "Admin User", + destination_contact_phone: "+85251111111", + destination_street_address: "Castle Peak Rd (So Kwun Wat)", + }) + } + + let(:response) { + { + "uuid" => "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + "status" => "pending", + "vehicle_type" => "van", + "payment_method" => "prepaid_wallet", + "price" => {"amount" => 15000, "currency" => "HKD"}, + "price_breakdown" => [{"key" => "fee", "amount" => 15000}] + } + } + + let(:order_attributes) { + { + 'vehicle_type': vehicle, + "schedule_at": 1609312208, + "pickup_location": [22.5029632, 114.1277213], + "destination_contact_name": "Admin User", + "destination_contact_phone": "+85251111111", + "destination_location": [56.8766894, 120.9891416], + "destination_street_address": "Castle Peak Rd (So Kwun Wat)", + "pickup_contact_name": "David", + "pickup_contact_phone": "+85251111112", + "pickup_street_address": "Street" + } + } + + it 'should trigger Gogox API' do + mock_object = instance_double(GogoxApi::Transport, order: response) + allow(GogoxApi::Transport).to receive(:new).with(order_attributes) + .and_return(mock_object) + expect(gogovan.book).to eq(response) + end + end + + context "status" do + let(:response) { + { + "uuid" => "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + "status" => "pending", + "vehicle_type" => "van", + "payment_method" => "prepaid_wallet", + "price" => {"amount" => 15000, "currency" => "HKD"}, + "price_breakdown" => [{"key" => "fee", "amount" => 15000}] + } + } + + it 'should trigger Gogox API' do + mock_object = instance_double(GogoxApi::Transport, status: response) + allow(GogoxApi::Transport).to receive(:new).and_return(mock_object) + expect(Gogox.transport_status("2f859363-5c43-4fe2-9b91-6c6c43d610d2")).to eq(response) + end + end + + context "transport_status" do + let(:response) { + { + "uuid" => "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + "status" => "pending", + "vehicle_type" => "van", + "payment_method" => "prepaid_wallet", + "price" => {"amount" => 15000, "currency" => "HKD"}, + "price_breakdown" => [{"key" => "fee", "amount" => 15000}] + } + } + + it 'should trigger Gogox API' do + mock_object = instance_double(GogoxApi::Transport, status: response) + allow(GogoxApi::Transport).to receive(:new).and_return(mock_object) + expect(Gogox.transport_status("2f859363-5c43-4fe2-9b91-6c6c43d610d2")).to eq(response) + end + end + + context "cancel_order" do + let(:response) { + { + order_uuid: "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + status: "cancelled" + } + } + + it 'should trigger Gogox API' do + mock_object = instance_double(GogoxApi::Transport, cancel: nil) + allow(GogoxApi::Transport).to receive(:new).and_return(mock_object) + expect(Gogox.cancel_order("2f859363-5c43-4fe2-9b91-6c6c43d610d2")).to eq(response) + end + end + + describe 'parse_pickup_time' do + context 'when pickup date is not specified' do + it 'expects time to be in HKT' do + expect(Gogox.new.send(:parse_pickup_time).zone).to eq('HKT') + end + + it 'parse time to a DateTime object' do + expect(Gogox.new.send(:parse_time).class).to eq(Integer) + end + end + + context 'when pickup date is specified' do + it 'expects time to be in HKT' do + expect(Gogox.new({ schedule_at: Time.current.to_s }).send(:parse_pickup_time).zone).to eq('HKT') + end + + it 'parse time to a DateTime object' do + expect(Gogox.new({ schedule_at: Time.current.to_s }).send(:parse_time).class).to eq(Integer) + end + end + end + +end diff --git a/spec/services/transport_service_spec.rb b/spec/services/transport_service_spec.rb new file mode 100644 index 000000000..1be00735b --- /dev/null +++ b/spec/services/transport_service_spec.rb @@ -0,0 +1,139 @@ +require "rails_helper" + +describe TransportService do + + let(:schedule_at) { "Wed, 30 Dec 2020 15:10:08.000000000 +0800" } + let(:district) { (create :district) } + let(:vehicle) { "van" } + let(:user) { (create :user) } + let!(:provider) { (create :transport_provider, name: "GOGOX") } + let(:gogox_booking_id) { "2f859363-5c43-4fe2-9b91-6c6c43d610d2" } + + let!(:transport_order) { + create :transport_order, transport_provider: provider, + order_uuid: gogox_booking_id + } + + let(:gogox_booking_response) { + { + "uuid" => "2f859363-5c43-4fe2-9b91-6c6c43d610d2", + "status" => "pending", + "vehicle_type" => "van", + "payment_method" => "prepaid_wallet", + "price" => {"amount" => 15000, "currency" => "HKD"}, + "price_breakdown" => [{"key" => "fee", "amount" => 15000}], + "pickup" => { "schedule_at" => 7866788998 } + } + } + + let(:attributes) { + { + provider: "Gogox", + district_id: district.id, + vehicle_type: vehicle, + schedule_at: schedule_at, + user_id: user.id, + booking_id: gogox_booking_id + } + } + + let(:transport) { TransportService.new(attributes) } + + context "initialization" do + it "params" do + expect(transport.params).to eql(attributes) + end + it "district_id" do + expect(transport.district_id).to eql(district.id) + end + it "user" do + expect(transport.user).to eql(user) + end + it "provider" do + expect(transport.provider).to eql(Gogox) + end + end + + context "quotation" do + let(:response) { + { + "vehicle_type" => "van", + "estimated_price" => {"amount" => 15000, "currency" => "HKD"}, + "estimated_price_breakdown" => [{"key" => "fee", "amount" => 15000}] + } + } + + let(:quotation_attributes) { + { + 'vehicle_type': vehicle, + "schedule_at": schedule_at, + "pickup_location": [22.5029632, 114.1277213], + "destination_location": [56.8766894, 120.9891416] + } + } + + it 'should trigger Gogox Service' do + mock_object = instance_double(Gogox, quotation: response) + allow(Gogox).to receive(:new).with(quotation_attributes) + .and_return(mock_object) + expect(transport.quotation).to eq(response) + end + end + + context "book" do + let(:transport) { + TransportService.new({ + provider: "Gogox", + district_id: district.id, + vehicle_type: vehicle, + schedule_at: schedule_at, + user_id: user.id, + pickup_street_address: 'Road', + pickup_contact_name: 'Swati', + pickup_contact_phone: '+85251111118' + }) + } + + let(:order_attributes) { + { + destination_contact_name: "Swati", + destination_contact_phone: 85251111116, + destination_location: [56.8766894, 120.9891416], + destination_street_address: "Castle Road", + pickup_contact_name: "Swati", + pickup_contact_phone: "+85251111118", + pickup_location: [22.5029632, 114.1277213], + pickup_street_address: "Road", + schedule_at: "Wed, 30 Dec 2020 15:10:08.000000000 +0800", + vehicle_type: "van" + } + } + + it 'should trigger Gogox Service' do + mock_object = instance_double(Gogox, book: gogox_booking_response) + allow(Gogox).to receive(:new).with(order_attributes) + .and_return(mock_object) + expect(transport.book.metadata).to eq(gogox_booking_response) + end + end + + context "status" do + it 'should trigger Gogox Service' do + allow(Gogox).to receive(:transport_status).with(gogox_booking_id) + .and_return(gogox_booking_response) + expect(transport.status).to eq(transport_order) + end + end + + context "cancel" do + it 'should trigger Gogox Service' do + allow(Gogox).to receive(:cancel_order).with(gogox_booking_id) + .and_return({ + order_uuid: gogox_booking_id, + status: "cancelled" + }) + expect(transport.cancel).to eq(transport_order) + end + end + +end diff --git a/spec/support/env.rb b/spec/support/env.rb index 706c4ecf3..1da7e3b80 100644 --- a/spec/support/env.rb +++ b/spec/support/env.rb @@ -27,3 +27,7 @@ ENV['JWT_VALIDITY_FOR_API'] = "31536000" ENV['OTP_CODE_VALIDITY']="30" ENV['SOCKETIO_SERVICE_URL']="http://localhost:1337/send?site=goodcity&apiKey=132323" +ENV["CROSSROADS_GEOLOCATION"]="[56.8766894, 120.9891416]" +ENV["CROSSROADS_STREET_ADDRESS"]="Castle Road" +ENV["CROSSROADS_CONTACT_NAME"]="Swati" +ENV["CROSSROADS_CONTACT_PHONE"]="+85251111116"