Class: TicketService::Transaction

Inherits:
ApplicationService
  • Object
show all
Includes:
DefaultErrorContainer, OmiseHelper, OmiseProviderChecker
Defined in:
app/services/ticket_service/transaction.rb

Defined Under Namespace

Classes: TicketTransactionAdapter

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from OmiseProviderChecker

omise_provider?

Methods included from OmiseHelper

#configure_omise_keys, #omise_public_key

Methods included from DefaultErrorContainer

#error, #error_message_simple, #merge_errors

Constructor Details

#initialize(params, user = nil, ip_address = nil) ⇒ Transaction

Returns a new instance of Transaction.

Parameters:

  • params (ActionController::Parameters)

    Transaction parameters

  • user (User, nil) (defaults to: nil)

    The authenticated user (optional)

  • ip_address (String, nil) (defaults to: nil)

    Client IP address for payment processing (optional, but recommended for Omise credit card payments for fraud prevention)



13
14
15
16
17
18
19
20
# File 'app/services/ticket_service/transaction.rb', line 13

def initialize(params, user = nil, ip_address = nil)
  @params = params
  @user   = user
  @transaction = nil
  @expiry_time = nil
  @vendor_payment = params[:vendor_payment].presence || {}
  @ip_address = ip_address
end

Instance Attribute Details

#expiry_timeObject

Returns the value of attribute expiry_time.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def expiry_time
  @expiry_time
end

#ip_addressObject

Returns the value of attribute ip_address.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def ip_address
  @ip_address
end

#outcomeObject (readonly)

Returns the value of attribute outcome.



6
7
8
# File 'app/services/ticket_service/transaction.rb', line 6

def outcome
  @outcome
end

#paramsObject

Returns the value of attribute params.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def params
  @params
end

#transactionObject

Returns the value of attribute transaction.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def transaction
  @transaction
end

#userObject

Returns the value of attribute user.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def user
  @user
end

#vendor_paymentObject

Returns the value of attribute vendor_payment.



7
8
9
# File 'app/services/ticket_service/transaction.rb', line 7

def vendor_payment
  @vendor_payment
end

Instance Method Details

#createObject

same as #execute, but this is for Locking System feature this controller is to record user and payment data the TicketTransaction already paid by #build_charge_record method when for_locking_system is true, then it means the user hasn't start any payment process



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'app/services/ticket_service/transaction.rb', line 200

def create
  if transaction.blank?
    errors.add :base, 'Failed to complete process, ticket transaction is blank'
  end

  result = false
  ActiveRecord::Base.transaction do
    unless transaction.active?
      raise InvalidVoucherPurchase, 'Sorry, No active transaction found'
    end

    @params = params.tap do |h|
      h[:ticket_groups] = []
    end

    form = TicketService::Form.new(params, user)
    form.error_container = errors
    raise ActiveRecord::Rollback, form.error_message_simple unless form.valid?

    transaction.assign_attributes(form.transaction_attributes)

    transaction.ticket_bundles.each do |tb|
      if tb.ticket_group.total_orders > tb.ticket_group.quantity
        raise ActiveRecord::Rollback, "We're sorry this voucher is not available anymore"
      end
    end

    # since guest params provided in the second step, we need to update guest_id for each ticket
    if transaction.guest.present?
      transaction.tickets.each do |ticket|
        ticket.guest_id = transaction.guest_id
      end
    end

    transaction.active = true
    transaction.for_locking_system = false

    transaction.charge_price = if form.vendor_payment?
                                 vendor_payment[:amount_cents].to_i / 100
                               else
                                 transaction.total_price # TODO: update after implement voucher
                               end
    unless transaction.save
      merge_errors(transaction.errors)
      raise ActiveRecord::Rollback
    end

    update_firebase(transaction) # initial create pending payment status

    raise ActiveRecord::Rollback unless build_charge_record(transaction, form)

    unless transaction.save
      merge_errors(transaction.errors)
      raise ActiveRecord::Rollback
    end

    if transaction.status_as_symbol == :paid
      activate_tickets(transaction, form)
    end
    @outcome = transaction
    result = true
  rescue UncaughtThrowError
    raise ActiveRecord::Rollback, 'Failed to complete process, ticket transaction quota is not available'
  end

  if result
    update_firebase(@outcome) # update payment status
    count_down = AdminSetting.prompt_pay_count_down_in_minute
    [1, 5, 7, 10].each do |number|
      Workers::Payments::CheckPaymentWorker.perform_in(number.to_i.minutes, :ticket_transaction, @outcome.id)
    end
    Workers::Payments::CancelLaterWorker.perform_in(count_down.to_i.minutes, :ticket_transaction, @outcome.id)
  end

  result
rescue InvalidVoucherPurchase, ActiveRecord::Rollback => e
  count_down = 10.minutes
  Workers::TicketTransactions::CancelTemporaryWorker.perform_in(count_down, transaction.id, transaction.session_id)

  Rails.logger.error(e)
  errors.add(:base, e.message)
  false
end

#executeObject

this action is to accept TicketTransaction without Locking System we should remove this action after Locking System is implemented



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/services/ticket_service/transaction.rb', line 28

def execute
  result = false
  ActiveRecord::Base.transaction do
    ticket_groups = find_ticket_group_from_params
    ticket_groups.each &:lock!

    form = TicketService::OldForm.new(params, user)
    form.error_container = errors
    raise ActiveRecord::Rollback, form.error_message_simple unless form.valid?

    transaction = TicketTransaction.new(form.transaction_attributes)

    transaction.active = true
    transaction.charge_price = transaction.total_price # TODO: update after implement voucher

    unless transaction.save
      merge_errors(transaction.errors)
      raise ActiveRecord::Rollback
    end

    transaction.ticket_bundles.each do |ticket_bundle|
      ticket_group = ticket_groups.select { |tg| tg.id == ticket_bundle.ticket_group_id }.last
      total_quantity_sold = ticket_bundle.quantity
      ticket_group.total_orders = ticket_group.total_orders.to_i + total_quantity_sold

      # prevent overbook
      if ticket_group.total_orders > ticket_group.quantity
        errors.add :base, "We're sorry this product is not available anymore"
        raise ActiveRecord::Rollback
      end
    end

    transaction.mark_partner_tickets_as_on_keep!
    update_firebase(transaction) # initial create pending payment status

    raise ActiveRecord::Rollback unless build_charge_record(transaction, form)

    unless transaction.save
      merge_errors(transaction.errors)
      raise ActiveRecord::Rollback
    end

    if transaction.status_as_symbol == :paid
      activate_tickets(transaction, form)
      update_firebase(transaction) # update success payment status
    end
    @outcome = transaction

    ticket_groups.each &:save!
    result = true

    if result
      count_down = AdminSetting.prompt_pay_count_down_in_minute
      [1, 5, 7, 10].each do |number|
        Workers::Payments::CheckPaymentWorker.perform_in(number.to_i.minutes, :ticket_transaction, transaction.id)
      end
      Workers::Payments::CancelLaterWorker.perform_in(count_down.to_i.minutes, :ticket_transaction, transaction.id)
    end
  end

  result
rescue InvalidVoucherPurchase, ActiveRecord::Rollback => e
  errors.add(:base, e.message)
  false
end

#lockObject



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'app/services/ticket_service/transaction.rb', line 94

def lock
  success = false
  fetched_tickets = {}

  # we use session_id to track the locking request ID
  # if the request ID is not the same, then there is a new request updating the session ID value
  new_session_id = nil
  ticket_groups = TicketGroup.where(id: params[:ticket_groups].pluck(:id).compact)

  quota_witness = InvQuotaWitness.new
  keys_to_watch = ticket_groups.map { |tg| quota_witness.quota_key(tg.id) }
  quota_witness.transaction(*keys_to_watch.join(',')) do |redis|
    ActiveRecord::Base.transaction do
      form = TicketService::Form.new(params, user)
      form.enable_locking_system
      form.error_container = errors
      raise ActiveRecord::Rollback, form.error_message_simple unless form.valid?

      transaction = TicketTransaction.new(form.transaction_attributes)
      transaction.active = true
      transaction.for_locking_system = true
      transaction.charge_price = transaction.total_price # TODO: update after implementing vouchers

      all_available = true

      # Check availability for each event
      transaction.ticket_bundles.each do |ticket_bundle|
        tickets = quota_witness.pick_tickets(ticket_bundle.ticket_group_id, ticket_bundle.quantity, redis)

        if tickets.length == ticket_bundle.quantity
          fetched_tickets[ticket_bundle.ticket_group_id] = tickets
          ticket_bundle.booked_quantity = tickets
        else
          all_available = false
          break
        end
      end

      if all_available
        result = []
        redis.multi do |multi|
          fetched_tickets.each do |ticket_group_id, tickets|
            tickets.each do |ticket|
              result.push multi.srem(quota_witness.quota_key(ticket_group_id), ticket)
            end
          end
        end

        if result
          Rails.logger.warn 'Transaction executed: User got tickets for multiple events.'
          transaction.restaurant_id = params[:restaurant_id]
          transaction.mark_partner_tickets_as_on_keep!
          transaction.save!

          ticket_groups.each &:save!
          new_session_id = transaction.renew_session_id
          transaction.save!

          @outcome = transaction
          success = true
        else
          error_message = 'Conflict'
          errors.add(:base, error_message)
          warn_message = "Transaction aborted: #{error_message}"
          Rails.logger.warn warn_message
          redis.unwatch
          raise ActiveRecord::Rollback, warn_message
        end
      else
        error_message = 'Insufficient available tickets'
        errors.add(:base, error_message)
        warn_message = "Transaction aborted: #{error_message}"
        Rails.logger.warn warn_message
        redis.unwatch
        raise ActiveRecord::Rollback, warn_message
      end
    rescue ActiveRecord::Deadlocked
      error_message = 'Transaction aborted: please try again later'
      errors.add(:base, error_message)
      raise ActiveRecord::Rollback, error_message
    end
  end

  if success && @outcome.present?
    count_down = Time.zone.now + AdminSetting.reservation_session_timeout.to_i.minutes
    @expiry_time = count_down.utc.iso8601
    Workers::TicketTransactions::CancelTemporaryWorker.perform_in(count_down, @outcome.id, new_session_id)
  elsif fetched_tickets.present?
    fetched_tickets.each do |ticket_group_id, tickets|
      Workers::TicketTransactions::RestoreQuotaWorker.perform_async(ticket_group_id, tickets)
    end
  end

  success
rescue StandardError => e
  Rails.logger.error(e)
  fetched_tickets.each do |ticket_group_id, tickets|
    Workers::TicketTransactions::RestoreQuotaWorker.perform_async(ticket_group_id, tickets)
  end
  false
end

#set_transaction(transaction_id) ⇒ Object



22
23
24
# File 'app/services/ticket_service/transaction.rb', line 22

def set_transaction(transaction_id)
  @transaction = TicketTransaction.find_by(id: transaction_id)
end