Class: VendorsService::GenerateMemoService

Inherits:
BaseOperationService show all
Includes:
DefaultErrorContainer, ElasticAPM::SpanHelpers, MoneyRails::ActionViewExtension
Defined in:
app/services/vendors_service/generate_memo_service.rb

Overview

Service for creating memo/notes for our inventory suppliers

Instance Attribute Summary collapse

Attributes inherited from BaseOperationService

#outcome

Instance Method Summary collapse

Methods included from DefaultErrorContainer

#error, #error_message_simple, #merge_errors

Methods inherited from BaseOperationService

#success?

Constructor Details

#initialize(reservation = nil, is_vendor_prepaid_reservation = false, reference_id = nil, reseller_reference_id = nil) ⇒ GenerateMemoService

Returns a new instance of GenerateMemoService.



11
12
13
14
15
16
17
# File 'app/services/vendors_service/generate_memo_service.rb', line 11

def initialize(reservation = nil, is_vendor_prepaid_reservation = false, reference_id = nil, reseller_reference_id = nil)
  super()
  @reservation = reservation
  @is_vendor_prepaid_reservation = is_vendor_prepaid_reservation
  @reference_id = reference_id
  @reseller_reference_id = reseller_reference_id
end

Instance Attribute Details

#reservationObject (readonly)

Returns the value of attribute reservation.



9
10
11
# File 'app/services/vendors_service/generate_memo_service.rb', line 9

def reservation
  @reservation
end

Instance Method Details

#build_other_payment_info(reservation, discount_cents, discount, prepaid_amount, due_amount, due_amount_int) ⇒ Object

Builds payment info string for other payment scenarios.



268
269
270
271
272
273
274
275
# File 'app/services/vendors_service/generate_memo_service.rb', line 268

def build_other_payment_info(reservation, discount_cents, discount, prepaid_amount, due_amount, due_amount_int)
  payment_info = ''
  payment_info += "Discount: #{discount_cents}\n" if discount > 0 && reservation.created_by.to_sym == :user
  total_prepaid = reservation.paid_amount.to_i > 0 ? prepaid_amount : total_amount(reservation)
  payment_info += "PrePaid: #{total_prepaid}\n"
  payment_info += "Due: #{due_amount}\n" if due_amount_int > 0
  payment_info
end

#build_paid_by_voucher_full_info(discount_cents, discount) ⇒ Object

Builds payment info string for reservations fully paid by voucher.



252
253
254
255
256
# File 'app/services/vendors_service/generate_memo_service.rb', line 252

def build_paid_by_voucher_full_info(discount_cents, discount)
  payment_info = "100% Fully Paid by promo code\n"
  payment_info += "Discount: #{discount_cents}\n" if discount > 0
  payment_info
end

#build_paid_by_voucher_partial_info(reservation, paid_percentage, discount_cents, discount, prepaid_amount, due_amount, due_amount_int) ⇒ Object

Builds payment info string for reservations partially paid by voucher.



259
260
261
262
263
264
265
# File 'app/services/vendors_service/generate_memo_service.rb', line 259

def build_paid_by_voucher_partial_info(reservation, paid_percentage, discount_cents, discount, prepaid_amount, due_amount, due_amount_int)
  payment_info = "#{paid_percentage}% Paid\n"
  payment_info += "Discount: #{discount_cents}\n" if discount > 0
  payment_info += "PrePaid: #{prepaid_amount}\n" if reservation.paid_amount.to_i > 0
  payment_info += "Due: #{due_amount}\n" if due_amount_int > 0
  payment_info
end

#build_partial_paid_payment_info(reservation, paid_percentage, discount_cents, discount, prepaid_amount, due_amount, due_amount_int, charge_percent) ⇒ Object

Builds payment info string for partially paid reservations. Handles due, prepaid, and discount details. Refactored for readability, complexity, and style compliance.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/services/vendors_service/generate_memo_service.rb', line 232

def build_partial_paid_payment_info(reservation, paid_percentage, discount_cents, discount, prepaid_amount, due_amount, due_amount_int, charge_percent)
  having_due = reservation.paid_amount.positive? &&
    due_amount_int.positive? &&
    due_amount_int.to_i < reservation.total_amount.to_i

  payment_info = ''
  if having_due || (charge_percent.positive? && charge_percent < 100)
    payment_info += "#{paid_percentage}% Paid\n"
  elsif reservation.prepaid_full?
    payment_info += "100% Fully Paid\n"
  end

  payment_info += "Discount: #{discount_cents}\n" if discount > 0

  payment_info += "PrePaid: #{prepaid_amount}\n" if reservation.paid_amount.to_i > 0
  payment_info += "Due: #{due_amount}\n" if due_amount_int > 0
  payment_info
end

#calculate_reservation_amounts(reservation) ⇒ Object



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'app/services/vendors_service/generate_memo_service.rb', line 277

def calculate_reservation_amounts(reservation)
  adult = reservation.adult
  kids  = reservation.kids
  reservation_date = reservation.date
  vendor_channel = reservation.booking_channel
  restaurant = reservation.restaurant

  calculator = HhPackage::ReservationPackages::ChargeCalculator.new

  formatted_packages = get_formatted_packages

  rest_packs_map = restaurant_packages_map(formatted_packages)

  package_bought = formatted_packages.map do |pack_bought|
    rest_pack_id = pack_bought['restaurant_package_id'] || pack_bought[:restaurant_package_id]
    rest_pack = rest_packs_map[rest_pack_id]

    if rest_pack.nil?
      BUSINESS_LOGGER.error('Restaurant package not found', package_id: rest_pack_id)
      APMErrorHandler.report('Missing restaurant package in vendor payment calculation', package_id: rest_pack_id)
      next
    end

    pack_bought['package'] = rest_pack.package
    pack_bought
  end.compact

  calculator.use_dynamic_pricing = vendor_channel&.support_dynamic_pricing || false

  begin
    data = calculator.calculate(package_bought, adult, kids, 0.0, restaurant,
                                reservation_date)
    data.presence
  rescue StandardError => e
    BUSINESS_LOGGER.error('Error calculating reservation amount', error: e.message, reservation_id: reservation.id)
    APMErrorHandler.report('Calculator error in vendor payment calculation', error: e.message)
    {}
  end
end

#discount_amount(amount, currency) ⇒ Object



193
194
195
# File 'app/services/vendors_service/generate_memo_service.rb', line 193

def discount_amount(amount, currency)
  money_with_currency(amount, currency)
end

#due_amount(reservation) ⇒ Object



189
190
191
# File 'app/services/vendors_service/generate_memo_service.rb', line 189

def due_amount(reservation)
  money_with_currency(reservation.due_amount, reservation.package_price_currency)
end

#executeObject



19
20
21
22
23
24
25
26
27
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
# File 'app/services/vendors_service/generate_memo_service.rb', line 19

def execute
  BUSINESS_LOGGER.set_business_context(reservation_id: reservation&.id)

  if reservation.nil?
    BUSINESS_LOGGER.error('Generated Supplier Notes/Memo', error: 'Reservation is nil')
    return ''
  end
  special_request = reservation.special_request_without_package_menus
  occasion = reservation.dining_occasion
  notes = []
  return notes if reservation.blank?

  notes << package_names(reservation)
  notes << "Big Group Booking\n" if reservation.group_booking?
  notes << payment_method(reservation)

  notes << "Occasion: #{occasion.name_en}\n" if occasion.present?

  notes << "Total Price: #{total_amount(reservation)}\n"
  notes << "\nCustomer Request: #{special_request}\n" if special_request.present?

  notes << if reservation.old_reservation_id.present?
             "\nReservation Old ID: #{reservation.old_reservation_id}\nReservation New ID: #{reservation.id}\n"
           else
             "\nReservation ID: #{reservation.id}\n"
           end

  is_show_in_owner_dashboard = reservation&.booking_channel&.show_in_owner_dashboard?

  if is_show_in_owner_dashboard
    vendor_name = parsed_vendor_name(reservation)
    notes << "Vendor: #{vendor_name}\n" if vendor_name.present?

    vendor_reference_id = reservation.vendor_reservation&.reference_id || @reference_id
    notes << "Vendor Reference ID: #{vendor_reference_id}\n" if vendor_reference_id.present?

    reseller_reference_id = reservation.vendor_reservation&.reseller_reference_id || @reseller_reference_id
    notes << "Reseller Reference ID: #{reseller_reference_id}\n" if reseller_reference_id.present?
  else
    notes << "Vendor: Hungry Hub\n"
  end

  owner = reservation.restaurant.owner
  notes << Rails.application.routes.url_helpers.owner_confirmation_reservation_url(owner_hash: owner.to_url_hash,
                                                                                   reservation_hash: reservation.to_url_hash)
  BUSINESS_LOGGER.info('Generated Supplier Notes/Memo', memo: notes)
  notes.join('')
rescue StandardError => e
  BUSINESS_LOGGER.error('Error in GenerateMemoService', error: e.message)
  APMErrorHandler.report(e, reservation_id: reservation&.id)
end

#get_formatted_packagesObject

Returns memoized formatted packages array (never nil) to avoid repeated chained calls.



318
319
320
# File 'app/services/vendors_service/generate_memo_service.rb', line 318

def get_formatted_packages
  @formatted_packages ||= reservation.package_obj&.formatted_packages || []
end

#money_with_currency(price, currency) ⇒ Object



197
198
199
# File 'app/services/vendors_service/generate_memo_service.rb', line 197

def money_with_currency(price, currency)
  HhMoney.new(price, currency).default_format
end

#package_names(reservation) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'app/services/vendors_service/generate_memo_service.rb', line 146

def package_names(reservation)
  formatted_packages = get_formatted_packages
  return if formatted_packages.blank?

  package_obj = reservation.package_obj&.type_short
  per_party_total = total_amount(reservation) # compute once
  packages = []
  formatted_packages.each do |package|
    net_price = HhMoney.new(package[:price_cents], package['price_currency']).default_format

    package_info = I18n.with_locale(:en) do
      I18n.t("views.booking.package_data_price.#{package_obj}", net_price: net_price,
                                                                quantity: package[:quantity])
    end

    package_name = I18n.with_locale(:en) do
      I18n.t("views.booking.package_data_name.#{package_obj}", name: package[:name])
    end
    package_detail = "(#{package_obj.upcase}) #{package_name} #{package_info} x #{reservation.party_size}\n#{per_party_total}"
    packages << package_detail
  end

  "Plan: #{packages.join(', ')}\n\n" if packages.present?
end

#parsed_vendor_name(reservation) ⇒ Object



201
202
203
204
205
206
# File 'app/services/vendors_service/generate_memo_service.rb', line 201

def parsed_vendor_name(reservation)
  vendor_name = reservation&.booking_channel&.name
  return '' if vendor_name.blank?

  vendor_name.to_s.split('HH x ').last
end

#payment_method(reservation) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
# File 'app/services/vendors_service/generate_memo_service.rb', line 72

def payment_method(reservation)
  package_price_currency = reservation.package_price_currency
  charge_type = reservation.charge_type

  paid_amount = reservation.paid_amount.to_i
  due_amount_int = reservation.due_amount.to_i
  total_amount = reservation.total_amount.to_i
  charge_percent = reservation.charge_percent.to_i
  prepaid_amount = prepaid_amount(reservation)
  due_amount = due_amount(reservation)

  active_reservation_vouchers = reservation.active_reservation_vouchers
  discount = active_reservation_vouchers.map(&:amount).sum.amount if active_reservation_vouchers.present?
  discount = discount.to_i
  discount_cents = HhMoney.new(discount, package_price_currency, from_amount: true).default_format
  total_paid = paid_amount + discount
  paid_percentage = total_amount.positive? ? ((total_paid.to_f / total_amount) * 100).to_i : 0

  payment_type_provider = reservation&.payment_type_provider

  if @is_vendor_prepaid_reservation
    calc_amounts = calculate_reservation_amounts(reservation)
    payment_info = "100% Fully Paid\n"
    if calc_amounts.present?
      payment_info += "PrePaid: #{money_with_currency(calc_amounts[:total_price_cents],
                                                      reservation.package_price_currency)}\n"
    end
    vendor_reservation_memo_payment_status = :paid
  elsif payment_type_provider.present? && payment_type_provider == reservation.channel_uri_name
    # for pre-paid vendors
    if reservation.created_by.to_sym == :user
      payment_info = "100% Fully Paid\n"
      payment_info += "PrePaid: #{prepaid_amount}\n" if reservation.paid_amount.to_i > 0
    elsif [:admin, :owner].include?(reservation.created_by.to_sym)
      payment_info = build_partial_paid_payment_info(reservation, paid_percentage,
                                                     discount_cents, discount, prepaid_amount, due_amount, due_amount_int, charge_percent)
    end
    vendor_reservation_memo_payment_status = :paid
  elsif reservation.status_as_symbol == :cancelled && (reservation&.cancel_reason&.include?('Payment canceled') || reservation&.payment_expired?)
    payment_info = "CANCEL BECAUSE OF PAYMENT FAILED\n"
    vendor_reservation_memo_payment_status = :failed
  elsif !reservation.payment_is_confirmed?
    # reservation requires payment but payment is not confirmed yet
    # this memo will be updated after payment is confirmed from mark_reservation_as_paid_service
    payment_info = "Payment is Pending\n"
    vendor_reservation_memo_payment_status = :pending
  elsif payment_type_provider.present? || reservation.paid_by_voucher_full? || reservation.paid_by_voucher_partially?
    if (charge_type.present? && reservation.paid_charges.present? && !reservation.paid_by_voucher_full?) ||
        (charge_type.present? && reservation.paid_charges.present? && !reservation.paid_by_voucher_partially?)
      payment_info = build_partial_paid_payment_info(reservation, paid_percentage, discount_cents, discount,
                                                     prepaid_amount, due_amount, due_amount_int, charge_percent)
    elsif reservation.paid_by_voucher_full?
      payment_info = build_paid_by_voucher_full_info(discount_cents, discount)
    elsif reservation.paid_by_voucher_partially?
      payment_info = build_paid_by_voucher_partial_info(reservation, paid_percentage, discount_cents, discount,
                                                        prepaid_amount, due_amount, due_amount_int)
    else
      payment_info = build_other_payment_info(reservation, discount_cents, discount, prepaid_amount, due_amount,
                                              due_amount_int)
    end
    payment_info += "Payment Method: #{payment_type_provider.to_s.split('_').join(' ').titleize}\n" if payment_type_provider.present? && reservation.created_by.to_sym == :user
    vendor_reservation_memo_payment_status = :paid
  else
    payment_info = "Pay at the restaurant\n#{total_amount(reservation)}\n"
    vendor_reservation_memo_payment_status = :postpaid
  end

  update_vendor_reservation_status(reservation, vendor_reservation_memo_payment_status)
  payment_info
rescue StandardError => e
  BUSINESS_LOGGER.error('Error in payment_method', error: e.message)
  APMErrorHandler.report(e, reservation_id: reservation&.id)
end

#prepaid_amount(reservation) ⇒ Object



185
186
187
# File 'app/services/vendors_service/generate_memo_service.rb', line 185

def prepaid_amount(reservation)
  money_with_currency(reservation.paid_amount, reservation.package_price_currency)
end

#restaurant_packages_map(formatted) ⇒ Object

Preloads all restaurant packages referenced by formatted packages to avoid N+1 DB queries. Returns a hash keyed by restaurant_package_id.



324
325
326
327
328
329
# File 'app/services/vendors_service/generate_memo_service.rb', line 324

def restaurant_packages_map(formatted)
  ids = formatted.map { |p| p['restaurant_package_id'] || p[:restaurant_package_id] }.compact
  return {} if ids.empty?

  HhPackage::RestaurantPackage.includes(:package, :restaurant).where(id: ids).index_by(&:id)
end

#total_amount(reservation) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'app/services/vendors_service/generate_memo_service.rb', line 171

def total_amount(reservation)
  package_price_currency = reservation.package_price_currency
  total_detail_amount = HhMoney.new(0, package_price_currency).default_format

  if reservation.package.present?
    total_net_amount = Money.from_amount(
      (reservation.total_amount - reservation.original_delivery_fee.to_d), package_price_currency
    )
    total_detail_amount = HhMoney.new(total_net_amount, package_price_currency).default_format
  end

  total_detail_amount
end

#update_vendor_reservation_status(reservation, vendor_reservation_memo_payment_status) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/services/vendors_service/generate_memo_service.rb', line 208

def update_vendor_reservation_status(reservation, vendor_reservation_memo_payment_status)
  vr = reservation.vendor_reservation
  return if vr.blank?

  vr.update!(supplier_memo_payment_status: vendor_reservation_memo_payment_status)
  BUSINESS_LOGGER.info(
    'Updated supplier_memo_payment_status',
    vendor_reservation_id: vr.id,
    status: vendor_reservation_memo_payment_status,
    reservation_id: reservation.id,
  )
rescue StandardError => e
  BUSINESS_LOGGER.error(
    'Failed to update supplier_memo_payment_status',
    error: e.message,
    reservation_id: reservation&.id,
    vendor_reservation_id: vr&.id,
  )
  APMErrorHandler.report(e, reservation_id: reservation&.id)
end