Class: Transport::PlaywrightPortalConnection

Inherits:
Object
  • Object
show all
Defined in:
app/services/transport/playwright_portal_connection.rb

Overview

Browser-based transport for partner portals that require form-based file uploads.
Uses Playwright to automate login, navigation, and file upload in a headless browser.

This is the transport equivalent of SFTP or HTTP API connections, but for
JavaScript SPAs that lack a programmatic API (e.g., Menard partner portal).

The Menard portal is a PingOne SSO JavaScript SPA — the login form is rendered
dynamically by main.bundle.js, not present in the initial HTML.

Constant Summary collapse

60_000
SPA_RENDER_TIMEOUT =
45_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ PlaywrightPortalConnection

Returns a new instance of PlaywrightPortalConnection.



19
20
21
22
23
24
25
# File 'app/services/transport/playwright_portal_connection.rb', line 19

def initialize(options = {})
  @options = options
  @headless = options.fetch(:headless, true)
  @logger = options[:logger] || Rails.logger
  @portal_base_url = options[:portal_base_url]
  @credentials = options[:credentials] || {}
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



17
18
19
# File 'app/services/transport/playwright_portal_connection.rb', line 17

def logger
  @logger
end

Instance Method Details

#send_file(file_path, remote_filepath = nil) ⇒ Hash

Uploads a file to the portal. Returns a result hash compatible with other transports.

Parameters:

  • file_path (String)

    Path to the local file to upload

  • remote_filepath (String) (defaults to: nil)

    URL of the upload page (ignored for Menard — hardcoded flow)

Returns:

  • (Hash)

    { success: Boolean, message: String, screenshot_path: String|nil }



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
# File 'app/services/transport/playwright_portal_connection.rb', line 31

def send_file(file_path, remote_filepath = nil)
  raise ArgumentError, "File not found: #{file_path}" unless File.exist?(file_path)

  logger.info("[PlaywrightPortal] Starting upload of #{File.basename(file_path)}")

  Playwright.create(playwright_cli_executable_path: playwright_cli_path) do |playwright|
    browser = playwright.chromium.launch(**launch_options)
    context = browser.new_context(
      userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' \
                 '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
    )
    page = context.new_page
    attach_diagnostics(page)

    begin
      active_page = (page)
      upload_file(active_page, file_path)
    rescue Playwright::Error, Playwright::TimeoutError => e
      dump_page_html(active_page || page, 'timeout_failure')
      logger.error("[PlaywrightPortal] #{e.class}: #{e.message}")
      { success: false, message: "Browser automation failed: #{e.message}" }
    rescue StandardError => e
      dump_page_html(active_page || page, 'unexpected_failure')
      logger.error("[PlaywrightPortal] #{e.class}: #{e.message}")
      { success: false, message: "Upload failed: #{e.message}" }
    end
  ensure
    browser&.close
  end
rescue StandardError => e
  logger.error("[PlaywrightPortal] Fatal: #{e.class}: #{e.message}")
  { success: false, message: "Upload failed: #{e.message}" }
end