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 generating standardized supplier notes/memos for reservations Outputs a formatted text memo with package, payment, and reservation details

Output format (4 sections, nil values auto-filtered):

SECTION 1: Package Information (Plan, Price, Qty, Total)
SECTION 2: Payment Information (Payment Status, PrePaid, Due, Discount, Payment Method)
SECTION 3: Additional Information (Customer Request, Occasion, Big Group Booking)
SECTION 4: Reservation Details (Reservation ID, Vendor, Reference ID, Reseller Reference ID, Reservation Link)

Examples:

Basic usage

VendorsService::GenerateMemoService.new(reservation).execute

Vendor prepaid reservation

VendorsService::GenerateMemoService.new(reservation, is_vendor_prepaid_reservation: true).execute

Constant Summary collapse

PAYMENT_STATUS_FULLY_PAID =

Payment status constants

'100% Fully Paid'
PAYMENT_STATUS_PENDING =
'Payment is Pending'
PAYMENT_STATUS_FAILED =
'CANCEL BECAUSE OF PAYMENT FAILED'
PAYMENT_STATUS_PAY_AT_RESTAURANT =
'Pay at the restaurant'
PAYMENT_STATUS_UNKNOWN =
'Unknown Status, pls. verify via Reservation Link'
PAYMENT_METHOD_PROMO_CODE =

Payment method constants

'Promo Code'
CANCEL_REASON_PAYMENT =

Cancel reason keyword

'Payment canceled'

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

Initialize the memo service

Parameters:

  • reservation (Reservation, nil) (defaults to: nil)

    The reservation to generate memo for

  • is_vendor_prepaid_reservation (Boolean) (defaults to: false)

    Whether this is a vendor prepaid reservation

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

    Optional vendor reference ID override

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

    Optional reseller reference ID override



46
47
48
49
50
51
52
# File 'app/services/vendors_service/generate_memo_service.rb', line 46

def initialize(reservation = nil, is_vendor_prepaid_reservation = false, reference_id = nil, reseller_reference_id = nil)
  super()
  @reservation = reservation&.decorate
  @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.



38
39
40
# File 'app/services/vendors_service/generate_memo_service.rb', line 38

def reservation
  @reservation
end

Instance Method Details

#build_standardized_memoString

Builds standardized memo with all possible fields organized in 4 sections Each section is a hash where nil values are automatically filtered out using compact

Returns:

  • (String)

    The formatted memo with sections separated by blank lines



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
# File 'app/services/vendors_service/generate_memo_service.rb', line 86

def build_standardized_memo
  section_package = {
    'Plan' => package_plan_line,
    'Price' => package_price_line,
    'Qty' => package_qty_line,
    'Total' => format_money(calculate_total_amount),
  }.compact

  section_payment = {
    'Payment Status' => payment_status_text,
    'PrePaid' => prepaid_value,
    'Due' => due_value,
    'Discount' => discount_value,
    'Payment Method' => payment_method_value,
  }.compact

  section_additional = {
    'Customer Request' => reservation.special_request_without_package_menus,
    'Occasion' => reservation.dining_occasion&.name_en,
    'Big Group Booking' => (reservation.group_booking? ? 'Yes' : nil),
  }.compact

  section_reservation = {
    'Reservation ID' => reservation_id_text,
    'Vendor' => vendor_name_text,
    'Reference ID' => reference_id_value,
    'Reseller Reference ID' => reseller_reference_id_value,
    'Reservation Link' => owner_confirmation_url,
  }.compact

  format_memo_sections([section_package, section_payment, section_additional, section_reservation])
end

#calculate_payment_status_percentageString?

Calculates payment percentage for partially paid reservations

Returns:

  • (String, nil)

    Percentage like “50% Paid” or “100% Fully Paid”, or nil



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'app/services/vendors_service/generate_memo_service.rb', line 349

def calculate_payment_status_percentage
  paid_amount = reservation.paid_amount.to_d
  total_amount_value = reservation.total_amount.to_d
  due_amount_value = reservation.due_amount.amount.to_d
  charge_percent = reservation.charge_percent

  active_vouchers = get_active_reservation_vouchers
  discount = active_vouchers.map(&:amount).sum.amount if active_vouchers.present?
  discount = discount.to_d
  total_paid = paid_amount + discount
  paid_percentage = total_amount_value.positive? ? ((total_paid / total_amount_value) * 100).to_i : 0

  having_due = paid_amount.positive? && due_amount_value.positive? && due_amount_value < total_amount_value

  if having_due || (charge_percent.positive? && charge_percent < 100)
    "#{paid_percentage}% Paid"
  elsif reservation.prepaid_full?
    PAYMENT_STATUS_FULLY_PAID
  end
end

#calculate_reservation_amounts(reservation) ⇒ Hash?

Calculates reservation amounts using ChargeCalculator for vendor prepaid reservations

Parameters:

Returns:

  • (Hash, nil)

    Calculation results with :total_price_cents key, or empty hash on error



578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
# File 'app/services/vendors_service/generate_memo_service.rb', line 578

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

#calculate_total_amountMoney

Calculates total amount excluding delivery fee

Returns:

  • (Money)

    Total amount as Money object



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
# File 'app/services/vendors_service/generate_memo_service.rb', line 230

def calculate_total_amount
  return @calculate_total_amount if defined?(@calculate_total_amount)

  formatted_packages = get_formatted_packages
  return @calculate_total_amount = Money.new(0, reservation.package_price_currency) if formatted_packages.blank?

  reservation_total = reservation.total_amount.to_f

  # If total_amount is zero/nil, calculate from packages (values are in cents)
  if reservation_total.zero?
    package_total_cents = formatted_packages.sum do |pkg|
      # Check if this is per_pack pricing
      restaurant_package = get_restaurant_package(pkg)
      is_per_pack = restaurant_package&.package&.dynamic_price_pricing_model&.to_sym == :per_pack

      if is_per_pack
        # For per_pack: only use total_adult_price (total_kids_price is the same value, not additive)
        pkg[:total_adult_price] || 0
      else
        # For per_person: sum adult and kids prices
        (pkg[:total_adult_price] || 0) + (pkg[:total_kids_price] || 0)
      end
    end
    reservation_total = package_total_cents / 100.0
  end

  delivery_fee = reservation.original_delivery_fee.to_d
  @calculate_total_amount = Money.from_amount(reservation_total - delivery_fee, reservation.package_price_currency)
end

#discount_valueString?

Returns formatted discount amount from vouchers if applicable

Returns:

  • (String, nil)

    Formatted amount like “฿50”, or nil if no discount



438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'app/services/vendors_service/generate_memo_service.rb', line 438

def discount_value
  active_vouchers = get_active_reservation_vouchers
  return nil if active_vouchers.blank?

  discount = active_vouchers.map(&:amount).sum.amount
  return nil if discount <= 0

  # Don't show discount for pay at restaurant unless user created
  if payment_status_text == PAYMENT_STATUS_PAY_AT_RESTAURANT && !(reservation.created_by.to_sym == :user)
    return nil
  end

  HhMoney.from_amount(discount, reservation.package_price_currency).default_format
end

#due_valueString?

Returns formatted due amount if applicable (not shown if 0)

Returns:

  • (String, nil)

    Formatted amount like “฿100”, or nil if 0 or not applicable



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'app/services/vendors_service/generate_memo_service.rb', line 415

def due_value
  # Don't show due for certain statuses
  return nil if reservation.status_as_symbol == :cancelled && (reservation&.cancel_reason&.include?(CANCEL_REASON_PAYMENT) || reservation&.payment_expired?)
  return nil if !reservation.payment_is_confirmed?
  return nil if reservation.paid_by_voucher_full?
  return nil if payment_status_text == PAYMENT_STATUS_FULLY_PAID

  due_amount_value = reservation.due_amount.amount.to_d

  # For "Pay at the restaurant" with no calculated due_amount, use the total as due
  if due_amount_value <= 0 && payment_status_text == PAYMENT_STATUS_PAY_AT_RESTAURANT
    total = calculate_total_amount
    return format_money(total) if total.positive?
  end

  return nil if due_amount_value <= 0

  format_money(reservation.due_amount)
end

#executeString

Generates and returns the supplier memo text

Returns:

  • (String)

    The formatted memo text, or empty string on error

Raises:

  • (ArgumentError)

    If reservation is nil



62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/services/vendors_service/generate_memo_service.rb', line 62

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

  raise ArgumentError, 'Reservation is nil' if reservation.nil?

  memo_text = build_standardized_memo
  BUSINESS_LOGGER.info('Generated Supplier Notes/Memo', memo: memo_text)
  memo_text
rescue StandardError => e
  clean_backtrace = Rails.backtrace_cleaner.clean(e.backtrace)
  BUSINESS_LOGGER.error('Error in GenerateMemoService', error: e.message, backtrace: clean_backtrace.first(5))
  APMErrorHandler.report(e, reservation_id: reservation&.id, backtrace: clean_backtrace)
  ''
end

#format_memo_sections(sections) ⇒ String

Formats sections into final memo output Each section is separated by a blank line, empty sections are filtered out

Parameters:

  • sections (Array<Hash>)

    Array of section hashes with key-value pairs

Returns:

  • (String)

    Formatted memo text with sections joined by blank lines



551
552
553
554
555
556
557
558
559
560
561
# File 'app/services/vendors_service/generate_memo_service.rb', line 551

def format_memo_sections(sections)
  formatted_sections = sections.map do |section|
    next if section.empty?

    section.map do |key, value|
      key == 'Reservation ID' ? value : "#{key}: #{value}"
    end.join("\n")
  end.compact

  formatted_sections.join("\n\n")
end

#format_money(amount_or_money) ⇒ String

Formats money value with currency symbol

Parameters:

  • amount_or_money (Integer, Money)

    Amount in cents or Money object

Returns:

  • (String)

    Formatted money like “฿210”



542
543
544
# File 'app/services/vendors_service/generate_memo_service.rb', line 542

def format_money(amount_or_money)
  HhMoney.new(amount_or_money, reservation.package_price_currency).default_format
end

#fully_paid?Boolean

Determines if reservation is fully paid (100%)

Returns:

  • (Boolean)

    True if reservation is 100% paid



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'app/services/vendors_service/generate_memo_service.rb', line 317

def fully_paid?
  # Vendor prepaid reservations are always fully paid
  return true if @is_vendor_prepaid_reservation

  payment_type_provider = reservation.payment_type_provider

  # Channel payments for user-created reservations
  if payment_type_provider.present? &&
      payment_type_provider == reservation.channel_uri_name &&
      reservation.created_by.to_sym == :user
    return true
  end

  # For admin/owner created reservations with channel payment, check if prepaid_full
  if payment_type_provider.present? &&
      payment_type_provider == reservation.channel_uri_name &&
      [:admin, :owner].include?(reservation.created_by.to_sym)
    return reservation.prepaid_full?
  end

  # Fully paid by voucher/promo code
  return true if reservation.paid_by_voucher_full?

  # Check if prepaid_full for other cases
  return true if reservation.prepaid_full?

  false
end

#get_active_reservation_vouchersArray

Returns active reservation vouchers

Returns:

  • (Array)

    Array of active reservation vouchers (never nil)



628
629
630
# File 'app/services/vendors_service/generate_memo_service.rb', line 628

def get_active_reservation_vouchers
  @active_reservation_vouchers ||= reservation.active_reservation_vouchers || []
end

#get_formatted_packagesArray<Hash>

Returns formatted packages array

Returns:

  • (Array<Hash>)

    Array of formatted package hashes (never nil)



621
622
623
# File 'app/services/vendors_service/generate_memo_service.rb', line 621

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

#get_restaurant_package(formatted_package) ⇒ HhPackage::RestaurantPackage?

Gets restaurant package for a formatted package with caching

Parameters:

  • formatted_package (Hash)

    Formatted package hash

Returns:



636
637
638
639
640
641
642
643
# File 'app/services/vendors_service/generate_memo_service.rb', line 636

def get_restaurant_package(formatted_package)
  package_with_indifferent_access = formatted_package.with_indifferent_access
  restaurant_package_id = package_with_indifferent_access[:restaurant_package_id]
  return nil if restaurant_package_id.blank?

  @restaurant_packages_cache ||= {}
  @restaurant_packages_cache[restaurant_package_id] ||= HhPackage::RestaurantPackage.includes(:package).find_by(id: restaurant_package_id)
end

#owner_confirmation_urlString

Returns owner confirmation URL for the reservation

Returns:

  • (String)

    Full URL to owner confirmation page



524
525
526
527
528
529
530
531
532
# File 'app/services/vendors_service/generate_memo_service.rb', line 524

def owner_confirmation_url
  @owner_confirmation_url ||= begin
    owner = reservation.restaurant.owner
    Rails.application.routes.url_helpers.owner_confirmation_reservation_url(
      owner_hash: owner.to_url_hash,
      reservation_hash: reservation.to_url_hash,
    )
  end
end

#package_plan_lineString?

Returns package plan name with type prefix

Examples:

"(AYCE) Neta Fuji"
"(BFP) Buffet Package"

Returns:

  • (String, nil)

    Package plan like “(AYCE) Neta Fuji”, or nil if no packages



130
131
132
133
134
135
136
137
138
139
140
# File 'app/services/vendors_service/generate_memo_service.rb', line 130

def package_plan_line
  formatted_packages = get_formatted_packages
  return nil if formatted_packages.blank?

  package_obj = reservation.package_obj&.type_short
  package_name = I18n.with_locale(:en) do
    I18n.t("views.booking.package_data_name.#{package_obj}", name: formatted_packages.first[:name])
  end

  "(#{package_obj&.upcase}) #{package_name}"
end

#package_price_lineString?

Returns package unit prices based on pricing model

Examples:

Per-person pricing

"฿70/Adult, ฿70/Kid"  # Different kid price
"฿70/Adult"           # No kids or same price

Per-pack pricing

"฿100/Pack"

Returns:

  • (String, nil)

    Price like “฿70/Adult, ฿70/Kid” or “฿100/Pack”, or nil if no packages



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
# File 'app/services/vendors_service/generate_memo_service.rb', line 150

def package_price_line
  formatted_packages = get_formatted_packages
  return nil if formatted_packages.blank?

  package = formatted_packages.first
  package_obj = reservation.package_obj

  # Determine if this is per_pack pricing using dynamic_price_pricing_model
  restaurant_package = get_restaurant_package(package)
  is_per_pack = restaurant_package&.package&.dynamic_price_pricing_model&.to_sym == :per_pack

  if is_per_pack
    # For per_pack: show price per pack
    pack_price = HhMoney.new(package[:price_cents], package['price_currency']).default_format
    "#{pack_price}/Pack"
  else
    # For per_person: show adult and optionally kid prices
    adult_price = HhMoney.new(package[:price_cents], package['price_currency']).default_format

    # Show kid price if there are kids AND either kids_amount > 0 OR total_kids_price exists
    has_kid_price = reservation.kids.positive? &&
      (package['kids_amount'].to_d.positive? || package[:total_kids_price].to_i.positive?)

    if has_kid_price
      kids_price = if package[:use_kids_price] == true
                     HhMoney.new(package[:kids_price_v2], package['price_currency']).default_format
                   elsif package[:total_kids_price].to_i.positive? && reservation.kids.positive?
                     # Calculate kid price from total_kids_price
                     kid_price_cents = (package[:total_kids_price].to_d / reservation.kids).to_i
                     HhMoney.new(kid_price_cents, package['price_currency']).default_format
                   else
                     adult_price
                   end
      "#{adult_price}/Adult, #{kids_price}/Kid"
    else
      "#{adult_price}/Adult"
    end
  end
end

#package_qty_lineString?

Returns package quantity based on pricing model

Examples:

Per-person pricing

"2 Adult, 1 Kid"   # With kids
"2 Adult"          # Adults only

Per-pack pricing

"2 Pack (4 Adult, 2 Kid)"  # Multiple packs with kids
"1 Pack (2 Adult)"           # Single pack

Returns:

  • (String, nil)

    Quantity like “2 Adult, 1 Kid” or “2 Pack (4 Adult, 2 Kid)”, or nil if no packages



199
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
# File 'app/services/vendors_service/generate_memo_service.rb', line 199

def package_qty_line
  formatted_packages = get_formatted_packages
  return nil if formatted_packages.blank?

  package = formatted_packages.first
  package_obj = reservation.package_obj

  # Determine if this is per_pack pricing using dynamic_price_pricing_model
  restaurant_package = get_restaurant_package(package)
  is_per_pack = restaurant_package&.package&.dynamic_price_pricing_model&.to_sym == :per_pack

  if is_per_pack
    # For per_pack: show "X Pack (Y Adult, Z Kid)" or "X Pack (Y Adult)"
    pack_quantity = package[:quantity] || 1

    if reservation.kids.positive?
      "#{pack_quantity} Pack (#{reservation.adult} Adult, #{reservation.kids} Kid)"
    else
      "#{pack_quantity} Pack (#{reservation.adult} Adult)"
    end
  elsif reservation.kids.positive?
    # For per_person: show "X Adult, Y Kid" or "X Adult"
    "#{reservation.adult} Adult, #{reservation.kids} Kid"
  else
    "#{reservation.adult} Adult"
  end
end

#parsed_vendor_name(reservation) ⇒ String

Extracts vendor name from booking channel name (removes “HH x ” prefix)

Parameters:

Returns:

  • (String)

    Vendor name without prefix, or empty string if blank



567
568
569
570
571
572
# File 'app/services/vendors_service/generate_memo_service.rb', line 567

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_valueString?

Returns payment method name if applicable

Returns:

  • (String, nil)

    Payment method like “Credit Card”, “Omise”, “Promo Code”, or nil



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'app/services/vendors_service/generate_memo_service.rb', line 456

def payment_method_value
  # Show "Promo Code" for voucher payments
  return PAYMENT_METHOD_PROMO_CODE if reservation.paid_by_voucher_full? || reservation.paid_by_voucher_partially?

  payment_type_provider = reservation&.payment_type_provider
  return nil if payment_type_provider.blank?
  return nil unless reservation.created_by.to_sym == :user
  # Only show if payment_type_provider is NOT the same as channel_uri_name
  return nil if payment_type_provider == reservation.channel_uri_name

  # Don't show for these statuses
  return nil if reservation.status_as_symbol == :cancelled && (reservation&.cancel_reason&.include?(CANCEL_REASON_PAYMENT) || reservation&.payment_expired?)
  return nil if !reservation.payment_is_confirmed?
  return nil if payment_status_text == PAYMENT_STATUS_PAY_AT_RESTAURANT

  payment_type_provider.to_s.split('_').join(' ').titleize
end

#payment_status_textString

Returns payment status text based on reservation state

Examples:

"100% Fully Paid"
"50% Paid"
"Payment is Pending"
"CANCEL BECAUSE OF PAYMENT FAILED"
"Pay at the restaurant"

Returns:

  • (String)

    Payment status like “100% Fully Paid”, “50% Paid”, “Payment is Pending”, etc.



273
274
275
276
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
# File 'app/services/vendors_service/generate_memo_service.rb', line 273

def payment_status_text
  return @payment_status_text if defined?(@payment_status_text)

  payment_type_provider = reservation&.payment_type_provider
  vendor_status = nil

  status_text = if reservation.status_as_symbol == :cancelled && (reservation&.cancel_reason&.include?(CANCEL_REASON_PAYMENT) || reservation&.payment_expired?)
                  # Cancelled due to payment failure - highest priority check
                  vendor_status = :failed
                  PAYMENT_STATUS_FAILED
                elsif fully_paid?
                  # Check for 100% fully paid scenarios (includes vendor prepaid override)
                  # This must come before payment_is_confirmed check to handle vendor prepaid reservations
                  vendor_status = :paid
                  PAYMENT_STATUS_FULLY_PAID
                elsif !reservation.payment_is_confirmed? || (reservation.paid_by_voucher_partially? && reservation.paid_amount.zero?)
                  # Payment not confirmed yet OR partial voucher without actual money payment
                  vendor_status = :pending
                  PAYMENT_STATUS_PENDING
                elsif payment_type_provider.present? ||
                    (reservation.model.payment_status == 'prepaid' && reservation.due_amount.amount > 0)
                  # Calculate percentage for partial payments
                  vendor_status = :paid
                  calculate_payment_status_percentage
                elsif reservation.model.payment_status == 'no_deposit' ||
                    (reservation.paid_amount.zero? && reservation.due_amount.amount > 0)
                  # Pay at restaurant - no prepayment required
                  vendor_status = :postpaid
                  PAYMENT_STATUS_PAY_AT_RESTAURANT
                else
                  # Unknown/unhandled payment status
                  vendor_status = :pending
                  PAYMENT_STATUS_UNKNOWN
                end

  # Update vendor reservation status
  update_vendor_reservation_status(reservation, vendor_status) if vendor_status.present?

  @payment_status_text = status_text
end

#prepaid_valueString?

Returns formatted prepaid amount if applicable

Returns:

  • (String, nil)

    Formatted amount like “฿210”, or nil if not applicable



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'app/services/vendors_service/generate_memo_service.rb', line 373

def prepaid_value
  # Don't show prepaid for these statuses
  return nil if reservation.status_as_symbol == :cancelled && (reservation&.cancel_reason&.include?(CANCEL_REASON_PAYMENT) || reservation&.payment_expired?)
  return nil if !reservation.payment_is_confirmed?
  return nil if reservation.paid_by_voucher_full?

  # Check if there's actually an amount paid before hiding PrePaid
  # Don't show prepaid if payment_status is "Pay at the restaurant" AND no amount has been paid yet
  if payment_status_text == PAYMENT_STATUS_PAY_AT_RESTAURANT && reservation.paid_amount.is_a?(Money) && reservation.paid_amount.zero?
    return nil
  end

  # For vendor prepaid reservations, calculate from packages
  if @is_vendor_prepaid_reservation
    calc_amounts = calculate_reservation_amounts(reservation)
    return format_money(calc_amounts[:total_price_cents]) if calc_amounts.present?
  end

  # Calculate prepaid amount from reservation
  paid = reservation.paid_amount
  if paid.is_a?(Money) && paid.zero? && (reservation.payment_type_provider.present? || @is_vendor_prepaid_reservation)
    formatted_packages = get_formatted_packages
    paid_cents = formatted_packages.sum { |pkg| (pkg[:total_adult_price] || 0) + (pkg[:total_kids_price] || 0) }
    paid = Money.new(paid_cents, reservation.package_price_currency)
  end

  # Format Money object properly
  prepaid_amt = if paid.is_a?(Money)
                  HhMoney.from_amount(paid.amount,
                                      paid.currency).default_format
                else
                  format_money(paid)
                end
  return nil if paid.is_a?(Money) && paid.zero?
  return nil if !paid.is_a?(Money) && prepaid_amt == format_money(0)

  prepaid_amt
end

#reference_id_valueString?

Returns vendor reference ID if available

Returns:

  • (String, nil)

    Vendor reference ID or nil



510
511
512
# File 'app/services/vendors_service/generate_memo_service.rb', line 510

def reference_id_value
  reservation.vendor_reservation&.reference_id || @reference_id
end

#reseller_reference_id_valueString?

Returns reseller reference ID if available

Returns:

  • (String, nil)

    Reseller reference ID or nil



517
518
519
# File 'app/services/vendors_service/generate_memo_service.rb', line 517

def reseller_reference_id_value
  reservation.vendor_reservation&.reseller_reference_id || @reseller_reference_id
end

#reservation_id_textString

Returns reservation ID text (handles old/new ID pairs for migrated reservations)

Examples:

Single ID

"Reservation ID: 3445354"

Dual ID (migrated reservation)

"Reservation Old ID: 1234567\nReservation New ID: 3445354"

Returns:

  • (String)

    Reservation ID text



485
486
487
488
489
490
491
# File 'app/services/vendors_service/generate_memo_service.rb', line 485

def reservation_id_text
  if reservation.old_reservation_id.present?
    "Reservation Old ID: #{reservation.old_reservation_id}\nReservation New ID: #{reservation.id}"
  else
    "Reservation ID: #{reservation.id}"
  end
end

#restaurant_packages_map(formatted) ⇒ Hash<Integer, HhPackage::RestaurantPackage>

Preloads all restaurant packages referenced by formatted packages to avoid N+1 queries

Parameters:

  • formatted (Array<Hash>)

    Array of formatted package hashes

Returns:



649
650
651
652
653
654
# File 'app/services/vendors_service/generate_memo_service.rb', line 649

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

#update_vendor_reservation_status(reservation, vendor_reservation_memo_payment_status) ⇒ void

This method returns an undefined value.

Updates vendor reservation supplier_memo_payment_status

Parameters:

  • reservation (Reservation)

    The reservation object

  • vendor_reservation_memo_payment_status (Symbol)

    Status to set (:paid, :pending, :failed, :postpaid)



661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'app/services/vendors_service/generate_memo_service.rb', line 661

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

#vendor_name_textString

Returns vendor name from booking channel or default

Returns:

  • (String)

    Vendor name like “Klook”, “Agoda”, or “Hungry Hub” as default



496
497
498
499
500
501
502
503
504
505
# File 'app/services/vendors_service/generate_memo_service.rb', line 496

def vendor_name_text
  is_show_in_owner_dashboard = reservation&.booking_channel&.show_in_owner_dashboard?

  if is_show_in_owner_dashboard
    vendor_name = parsed_vendor_name(reservation)
    vendor_name.presence || 'Hungry Hub'
  else
    'Hungry Hub'
  end
end