nti_receipt_builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +272 -0
  6. data/Rakefile +12 -0
  7. data/app/assets/javascripts/nti_receipt_builder/index.js +16 -0
  8. data/app/assets/javascripts/nti_receipt_builder/receipt_builder_controller.js +297 -0
  9. data/app/assets/javascripts/nti_receipt_builder/receipt_canvas_controller.js +279 -0
  10. data/app/assets/javascripts/nti_receipt_builder/receipt_draggable_controller.js +154 -0
  11. data/app/assets/javascripts/nti_receipt_builder/receipt_print_controller.js +24 -0
  12. data/app/assets/javascripts/nti_receipt_builder/receipt_properties_controller.js +297 -0
  13. data/app/assets/stylesheets/nti_receipt_builder.css +238 -0
  14. data/app/controllers/concerns/nti_receipt_builder/templates_controller.rb +170 -0
  15. data/app/helpers/nti_receipt_builder/render_helper.rb +21 -0
  16. data/app/models/nti_receipt_builder/template.rb +79 -0
  17. data/app/views/nti_receipt_builder/render/_order_lines_element.html.erb +36 -0
  18. data/app/views/nti_receipt_builder/render/_overflow_banner.html.erb +5 -0
  19. data/app/views/nti_receipt_builder/render/_paper.html.erb +12 -0
  20. data/app/views/nti_receipt_builder/render/_text_element.html.erb +1 -0
  21. data/app/views/nti_receipt_builder/render/_variable_element.html.erb +1 -0
  22. data/app/views/nti_receipt_builder/templates/_canvas.html.erb +33 -0
  23. data/app/views/nti_receipt_builder/templates/_elements_panel.html.erb +49 -0
  24. data/app/views/nti_receipt_builder/templates/_errors.html.erb +21 -0
  25. data/app/views/nti_receipt_builder/templates/_lock_version_field.html.erb +2 -0
  26. data/app/views/nti_receipt_builder/templates/_print_assets.html.erb +15 -0
  27. data/app/views/nti_receipt_builder/templates/_properties_panel.html.erb +8 -0
  28. data/app/views/nti_receipt_builder/templates/_settings_panel.html.erb +130 -0
  29. data/app/views/nti_receipt_builder/templates/create.turbo_stream.erb +3 -0
  30. data/app/views/nti_receipt_builder/templates/edit.html.erb +54 -0
  31. data/app/views/nti_receipt_builder/templates/new.html.erb +115 -0
  32. data/app/views/nti_receipt_builder/templates/preview.html.erb +24 -0
  33. data/app/views/nti_receipt_builder/templates/print.html.erb +31 -0
  34. data/app/views/nti_receipt_builder/templates/render_preview.turbo_stream.erb +26 -0
  35. data/app/views/nti_receipt_builder/templates/update.turbo_stream.erb +7 -0
  36. data/config/importmap.rb +4 -0
  37. data/lib/generators/nti_receipt_builder/install_generator.rb +49 -0
  38. data/lib/generators/nti_receipt_builder/templates/create_nti_receipt_templates.rb +28 -0
  39. data/lib/generators/nti_receipt_builder/templates/initializer.rb +16 -0
  40. data/lib/generators/nti_receipt_builder/templates/turbo_stream_tags.rb +36 -0
  41. data/lib/nti_receipt_builder/configuration.rb +59 -0
  42. data/lib/nti_receipt_builder/elements.rb +46 -0
  43. data/lib/nti_receipt_builder/engine.rb +32 -0
  44. data/lib/nti_receipt_builder/errors.rb +13 -0
  45. data/lib/nti_receipt_builder/layout_validator.rb +285 -0
  46. data/lib/nti_receipt_builder/presenters/collection.rb +126 -0
  47. data/lib/nti_receipt_builder/presenters/element.rb +68 -0
  48. data/lib/nti_receipt_builder/render_result.rb +18 -0
  49. data/lib/nti_receipt_builder/renderer.rb +78 -0
  50. data/lib/nti_receipt_builder/set_default.rb +24 -0
  51. data/lib/nti_receipt_builder/template_form.rb +60 -0
  52. data/lib/nti_receipt_builder/type_inference.rb +26 -0
  53. data/lib/nti_receipt_builder/variable_definition.rb +74 -0
  54. data/lib/nti_receipt_builder/variable_resolver.rb +55 -0
  55. data/lib/nti_receipt_builder/variables.rb +159 -0
  56. data/lib/nti_receipt_builder/version.rb +5 -0
  57. data/lib/nti_receipt_builder.rb +50 -0
  58. metadata +175 -0
@@ -0,0 +1,115 @@
1
+ <%= content_for :title, 'New Receipt Template' %>
2
+
3
+ <%= form_with model: @receipt_template, url: url_for(action: :create), scope: :receipt_template do |f| %>
4
+ <h3 class="card-title">Receipt Template</h3>
5
+ <div id="errors">
6
+ <% if @errors.present? %>
7
+ <%= render 'nti_receipt_builder/templates/errors', errors: @errors %>
8
+ <% end %>
9
+ </div>
10
+
11
+ <div class="row g-3">
12
+ <div class="col-md-6">
13
+ <div class="form-group mb-3">
14
+ <%= f.label :name, 'Template Name', class: 'form-label' %>
15
+ <%= f.text_field :name, class: 'form-control', placeholder: 'e.g. Main Branch Receipt' %>
16
+ </div>
17
+
18
+ <div class="form-group mb-3">
19
+ <%= f.label :paper_size_key, 'Paper Size', class: 'form-label' %>
20
+ <%= f.select :paper_size_key,
21
+ NtiReceiptBuilder::Template::PAPER_PRESETS.map { |key, preset| [preset[:label], key] },
22
+ {},
23
+ class: 'form-select' %>
24
+ </div>
25
+
26
+ <div class="row g-3">
27
+ <div class="col">
28
+ <div class="form-group mb-3">
29
+ <%= f.label :paper_width_mm, 'Width (mm)', class: 'form-label' %>
30
+ <%= f.number_field :paper_width_mm, step: 0.1,
31
+ min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
32
+ max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
33
+ class: 'form-control' %>
34
+ </div>
35
+ </div>
36
+ <div class="col">
37
+ <div class="form-group mb-3">
38
+ <%= f.label :paper_height_mm, 'Height (mm)', class: 'form-label' %>
39
+ <%= f.number_field :paper_height_mm, step: 0.1,
40
+ min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
41
+ max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
42
+ class: 'form-control' %>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="row g-3">
48
+ <div class="col">
49
+ <div class="form-group mb-3">
50
+ <%= f.label :orientation, 'Orientation', class: 'form-label' %>
51
+ <%= f.select :orientation,
52
+ NtiReceiptBuilder::Template::ORIENTATIONS.map { |value| [value.titleize, value] },
53
+ {},
54
+ class: 'form-select' %>
55
+ </div>
56
+ </div>
57
+ <div class="col">
58
+ <div class="form-group mb-3">
59
+ <%= f.label :height_mode, 'Height Mode', class: 'form-label' %>
60
+ <%= f.select :height_mode,
61
+ [['Fixed Height', 'fixed'], ['Grows With Content', 'content']],
62
+ {},
63
+ class: 'form-select' %>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="col-md-6">
70
+ <label class="form-label">Margins (mm)</label>
71
+ <div class="row g-3">
72
+ <div class="col-6">
73
+ <div class="form-group mb-3">
74
+ <%= f.label :margin_top_mm, 'Top', class: 'form-label' %>
75
+ <%= f.number_field :margin_top_mm, step: 0.1,
76
+ min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
77
+ class: 'form-control' %>
78
+ </div>
79
+ </div>
80
+ <div class="col-6">
81
+ <div class="form-group mb-3">
82
+ <%= f.label :margin_right_mm, 'Right', class: 'form-label' %>
83
+ <%= f.number_field :margin_right_mm, step: 0.1,
84
+ min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
85
+ class: 'form-control' %>
86
+ </div>
87
+ </div>
88
+ <div class="col-6">
89
+ <div class="form-group mb-3">
90
+ <%= f.label :margin_bottom_mm, 'Bottom', class: 'form-label' %>
91
+ <%= f.number_field :margin_bottom_mm, step: 0.1,
92
+ min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
93
+ class: 'form-control' %>
94
+ </div>
95
+ </div>
96
+ <div class="col-6">
97
+ <div class="form-group mb-3">
98
+ <%= f.label :margin_left_mm, 'Left', class: 'form-label' %>
99
+ <%= f.number_field :margin_left_mm, step: 0.1,
100
+ min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
101
+ class: 'form-control' %>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ <p class="text-secondary">
106
+ You'll drag variables, text, and the order-lines block onto the canvas on the next step.
107
+ </p>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="d-flex justify-content-end gap-2">
112
+ <%= link_to 'Cancel', url_for(action: :index), class: 'btn btn-secondary' %>
113
+ <%= f.submit 'Continue to Layout Builder', class: 'btn btn-primary' %>
114
+ </div>
115
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <% content_for :title, "Preview: #{@receipt_template.name}" %>
2
+ <% content_for :cardless, true %>
3
+
4
+ <div class="nrb-receipt-topbar">
5
+ <div>
6
+ <span class="nrb-receipt-topbar-name"><%= @receipt_template.name %></span>
7
+ <span class="text-secondary"><%= @receipt_template.paper_width_mm %>mm &times; <%= @receipt_template.paper_height_mm %>mm</span>
8
+ </div>
9
+ <div class="d-flex justify-content-end gap-2">
10
+ <%= link_to url_for(action: :print, id: @receipt_template.id),
11
+ class: 'btn btn-outline-secondary', target: '_blank', rel: 'noopener' do %>
12
+ <span class="material-symbols-outlined pe-1">print</span>Print
13
+ <% end %>
14
+ <%= link_to 'Back to Editor', url_for(action: :edit, id: @receipt_template.id), class: 'btn btn-secondary' %>
15
+ </div>
16
+ </div>
17
+
18
+ <% if @render_result.overflow? %>
19
+ <%= render 'nti_receipt_builder/render/overflow_banner' %>
20
+ <% end %>
21
+
22
+ <div class="nrb-receipt-preview-frame">
23
+ <%= render 'nti_receipt_builder/render/paper', receipt_template: @receipt_template, render_result: @render_result %>
24
+ </div>
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= @receipt_template.name %></title>
6
+
7
+ <%= render 'nti_receipt_builder/templates/print_assets' %>
8
+
9
+ <style>
10
+ @page {
11
+ size: <%= @receipt_template.paper_width_mm.to_f %>mm <%= @receipt_template.paper_height_mm.to_f %>mm;
12
+ margin: 0;
13
+ }
14
+
15
+ html, body {
16
+ margin: 0;
17
+ padding: 0;
18
+ background: #fff;
19
+ }
20
+
21
+ .nrb-receipt-paper-render {
22
+ margin: 0;
23
+ box-shadow: none;
24
+ border: none;
25
+ }
26
+ </style>
27
+ </head>
28
+ <body data-controller="nrb-print">
29
+ <%= render 'nti_receipt_builder/render/paper', receipt_template: @receipt_template, render_result: @render_result %>
30
+ </body>
31
+ </html>
@@ -0,0 +1,26 @@
1
+ <%= turbo_stream.open_modal size: 'modal-lg' do %>
2
+ <div class="modal-header">
3
+ <h1 class="modal-title">Live Preview</h1>
4
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
5
+ </div>
6
+ <div class="modal-body nrb-receipt-preview-modal">
7
+ <p class="text-secondary nrb-receipt-hint">
8
+ Rendered with sample data from your unsaved changes &mdash; nothing has been saved yet.
9
+ </p>
10
+
11
+ <% if @draft_errors.present? %>
12
+ <%= render 'nti_receipt_builder/templates/errors', errors: @draft_errors %>
13
+ <% else %>
14
+ <% if @render_result.overflow? %>
15
+ <%= render 'nti_receipt_builder/render/overflow_banner' %>
16
+ <% end %>
17
+
18
+ <div class="nrb-receipt-preview-frame">
19
+ <%= render 'nti_receipt_builder/render/paper', receipt_template: @draft, render_result: @render_result %>
20
+ </div>
21
+ <% end %>
22
+ </div>
23
+ <div class="modal-footer">
24
+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
25
+ </div>
26
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <% if @errors.empty? %>
2
+ <%= turbo_stream.toast('Receipt template saved!', type: :success) %>
3
+ <%= turbo_stream.replace 'lock_version_field', partial: 'nti_receipt_builder/templates/lock_version_field', locals: { receipt_template: @receipt_template } %>
4
+ <%= turbo_stream.update 'errors', '' %>
5
+ <% else %>
6
+ <%= turbo_stream.update 'errors', partial: 'nti_receipt_builder/templates/errors', locals: { errors: @errors } %>
7
+ <% end %>
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin_all_from NtiReceiptBuilder::Engine.root.join('app/assets/javascripts/nti_receipt_builder'),
4
+ under: 'nti_receipt_builder'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module NtiReceiptBuilder
7
+ module Generators
8
+ # rails generate nti_receipt_builder:install
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include ActiveRecord::Generators::Migration
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+
14
+ desc 'Creates the nti_receipt_builder initializer, migration, and (if absent) the ' \
15
+ 'Turbo Stream helpers the gem views depend on.'
16
+
17
+ def create_initializer
18
+ template 'initializer.rb', 'config/initializers/nti_receipt_builder.rb'
19
+ end
20
+
21
+ def create_migration_file
22
+ migration_template 'create_nti_receipt_templates.rb',
23
+ 'db/migrate/create_nti_receipt_templates.rb'
24
+ end
25
+
26
+ # Only for hosts that do not already extend Turbo::Streams::TagBuilder. A host that
27
+ # already defines open_modal has its own conventions; overwriting them would be wrong.
28
+ def create_turbo_stream_helpers
29
+ if turbo_stream_helpers_present?
30
+ say_status :skip, 'config/initializers/turbo_stream_tags.rb (host already defines open_modal)', :yellow
31
+ return
32
+ end
33
+
34
+ template 'turbo_stream_tags.rb', 'config/initializers/turbo_stream_tags.rb'
35
+ end
36
+
37
+ private
38
+
39
+ def turbo_stream_helpers_present?
40
+ defined?(Turbo::Streams::TagBuilder) &&
41
+ Turbo::Streams::TagBuilder.method_defined?(:open_modal)
42
+ end
43
+
44
+ def migration_version
45
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateNtiReceiptTemplates < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :nti_receipt_templates do |t|
6
+ t.string :owner_type, null: false
7
+ t.bigint :owner_id, null: false
8
+ t.string :name, null: false
9
+ t.string :paper_size_key, default: 'custom', null: false
10
+ t.string :orientation, default: 'portrait', null: false
11
+ t.string :height_mode, default: 'fixed', null: false
12
+ t.decimal :paper_width_mm, precision: 8, scale: 2, null: false
13
+ t.decimal :paper_height_mm, precision: 8, scale: 2, null: false
14
+ t.decimal :margin_top_mm, precision: 6, scale: 2, default: '0.0', null: false
15
+ t.decimal :margin_right_mm, precision: 6, scale: 2, default: '0.0', null: false
16
+ t.decimal :margin_bottom_mm, precision: 6, scale: 2, default: '0.0', null: false
17
+ t.decimal :margin_left_mm, precision: 6, scale: 2, default: '0.0', null: false
18
+ t.jsonb :layout, default: [], null: false
19
+ t.boolean :is_default, default: false, null: false
20
+ t.integer :lock_version, default: 0, null: false
21
+
22
+ t.timestamps
23
+ end
24
+
25
+ add_index :nti_receipt_templates, %i[owner_type owner_id],
26
+ name: 'index_nti_receipt_templates_on_owner'
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ NtiReceiptBuilder.configure do |config|
4
+ # Required. A NtiReceiptBuilder::Variables subclass declaring this application's receipt
5
+ # placeholders. Assign the class or its name — the gem stores only the name, so a reloadable
6
+ # application class is never pinned across a code reload.
7
+ # config.variables_class = 'OrderReceiptVariables'
8
+
9
+ # Optional. Point at a subclass of NtiReceiptBuilder::Template to attach your own concerns
10
+ # (audit, status, tenancy) while sharing the same table.
11
+ # config.template_class = 'ReceiptTemplate'
12
+
13
+ # Optional formatting.
14
+ # config.currency_unit = '₱'
15
+ # config.datetime_format = '%m/%d/%Y %I:%M %p'
16
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Turbo Stream actions nti_receipt_builder's views emit. Pair them with matching
4
+ # turbo_stream action handlers in your JavaScript (app_modal, close_modal, toast,
5
+ # reload_datatable). Delete this file if your application already provides them.
6
+ Turbo::Streams::TagBuilder.class_eval do
7
+ def open_modal(size: 'modal-md', &)
8
+ content = @view_context.capture(&)
9
+
10
+ @view_context.turbo_stream_action_tag(
11
+ :app_modal,
12
+ size: size,
13
+ template: content
14
+ )
15
+ end
16
+
17
+ def close_modal
18
+ @view_context.turbo_stream_action_tag(:close_modal)
19
+ end
20
+
21
+ def toast(message, type: :success)
22
+ @view_context.turbo_stream_action_tag(
23
+ :toast,
24
+ message: message,
25
+ type: type
26
+ )
27
+ end
28
+
29
+ def reload_datatable(table_id, url: nil)
30
+ @view_context.turbo_stream_action_tag(
31
+ :reload_datatable,
32
+ 'table-id': table_id,
33
+ url: url
34
+ )
35
+ end
36
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NtiReceiptBuilder
4
+ # Runtime configuration.
5
+ #
6
+ # Class references are held by NAME, never as Class objects. Retaining a reloadable
7
+ # application class here would pin that class and its whole constant tree past every
8
+ # development reload, and would serve a stale class after the host reloads its code.
9
+ class Configuration
10
+ DEFAULT_CURRENCY_UNIT = '₱'
11
+ DEFAULT_DATETIME_FORMAT = '%m/%d/%Y %I:%M %p'
12
+ DEFAULT_TEMPLATE_CLASS = 'NtiReceiptBuilder::Template'
13
+ DEFAULT_PRINT_STYLESHEET = 'nti_receipt_builder'
14
+
15
+ attr_accessor :currency_unit, :datetime_format
16
+ # Stylesheet(s) the standalone print document loads — a name or an array of names. Defaults
17
+ # to the gem's own so print output is correct without host configuration. A host whose
18
+ # bundle does not include this gem's CSS should list both, e.g. ['app', 'nti_receipt_builder'].
19
+ attr_accessor :print_stylesheet
20
+ attr_reader :variables_class_name, :template_class_name
21
+
22
+ def initialize
23
+ @currency_unit = DEFAULT_CURRENCY_UNIT
24
+ @datetime_format = DEFAULT_DATETIME_FORMAT
25
+ @print_stylesheet = DEFAULT_PRINT_STYLESHEET
26
+ @variables_class_name = nil
27
+ @template_class_name = DEFAULT_TEMPLATE_CLASS
28
+ end
29
+
30
+ def variables_class=(value)
31
+ @variables_class_name = class_name_for(value)
32
+ end
33
+
34
+ def variables_class
35
+ if @variables_class_name.nil?
36
+ raise NotConfiguredError,
37
+ 'NtiReceiptBuilder.config.variables_class must be set to a NtiReceiptBuilder::Variables subclass'
38
+ end
39
+
40
+ @variables_class_name.constantize
41
+ end
42
+
43
+ def template_class=(value)
44
+ @template_class_name = class_name_for(value)
45
+ end
46
+
47
+ def template_class
48
+ @template_class_name.constantize
49
+ end
50
+
51
+ private
52
+
53
+ def class_name_for(value)
54
+ return value.to_s unless value.is_a?(Class)
55
+
56
+ value.name || raise(Error, 'cannot configure an anonymous class — give it a name or pass a String')
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NtiReceiptBuilder
4
+ # Canonical whitelist of layout-element shapes, style properties and size limits a
5
+ # template's jsonb `layout` may contain. LayoutValidator is the only reader.
6
+ #
7
+ # Column keys are deliberately NOT here — they are declared by the host's variables class
8
+ # on the collection variable, so the gem carries no domain vocabulary.
9
+ #
10
+ # MAX_ELEMENTS and the length caps are what bound the memory a single template can consume;
11
+ # they are enforced before persistence, not by convention.
12
+ module Elements
13
+ TYPES = %w[variable text order_lines].freeze
14
+
15
+ MAX_ELEMENTS = 60
16
+ MIN_ELEMENT_DIMENSION_MM = 1.0
17
+
18
+ Z_INDEX_MIN = 0
19
+ Z_INDEX_MAX = 1000
20
+
21
+ TEXT_ALIGNS = %w[left center right].freeze
22
+ FONT_WEIGHTS = %w[normal bold].freeze
23
+
24
+ STYLE_WHITELIST = {
25
+ 'font_size_pt' => { type: :numeric, min: 6, max: 72 },
26
+ 'font_weight' => { type: :enum, values: FONT_WEIGHTS },
27
+ 'text_align' => { type: :enum, values: TEXT_ALIGNS },
28
+ 'line_height' => { type: :numeric, min: 0.8, max: 3.0 }
29
+ }.freeze
30
+
31
+ TEXT_MAX_LENGTH = 500
32
+ FALLBACK_MAX_LENGTH = 200
33
+ PREFIX_MAX_LENGTH = 50
34
+ SUFFIX_MAX_LENGTH = 50
35
+
36
+ # There is deliberately no maximum: the host declares the column vocabulary, and
37
+ # LayoutValidator rejects unknown and duplicated keys, so that vocabulary is already the
38
+ # ceiling. A constant here would reject a host whose collection is simply wider than the
39
+ # gem happened to anticipate.
40
+ MIN_ORDER_LINE_COLUMNS = 1
41
+ COLUMN_LABEL_MAX_LENGTH = 40
42
+
43
+ ROW_SPACING_MIN_MM = 0.0
44
+ ROW_SPACING_MAX_MM = 10.0
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NtiReceiptBuilder
4
+ # Non-isolated on purpose: host controllers include NtiReceiptBuilder::TemplatesController
5
+ # and must resolve this engine's views through the ordinary view-path chain. A non-isolated
6
+ # engine contributes app/views, app/models, app/helpers and app/controllers/concerns to the
7
+ # host automatically.
8
+ class Engine < ::Rails::Engine
9
+ # Propshaft picks up app/assets automatically; javascripts needs naming explicitly so the
10
+ # Stimulus controllers are servable.
11
+ initializer 'nti_receipt_builder.assets' do |app|
12
+ next unless app.config.respond_to?(:assets)
13
+
14
+ app.config.assets.paths << root.join('app/assets/javascripts')
15
+ app.config.assets.paths << root.join('app/assets/stylesheets')
16
+ end
17
+
18
+ initializer 'nti_receipt_builder.importmap', before: 'importmap' do |app|
19
+ next unless app.config.respond_to?(:importmap)
20
+
21
+ app.config.importmap.paths << root.join('config/importmap.rb')
22
+ end
23
+
24
+ # The render partials are rendered from whichever host controller prints a receipt, not
25
+ # just the builder's own, so paper_style has to be available everywhere.
26
+ initializer 'nti_receipt_builder.helpers' do
27
+ ActiveSupport.on_load(:action_controller_base) do
28
+ helper NtiReceiptBuilder::RenderHelper
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NtiReceiptBuilder
4
+ class Error < StandardError; end
5
+
6
+ # Raised when the gem is used before required configuration has been supplied.
7
+ class NotConfiguredError < Error; end
8
+
9
+ # Raised when a Variables subclass is malformed — MAPPINGS missing or not a Hash, a mapped
10
+ # method that does not exist, an override for a key absent from MAPPINGS, or more than one
11
+ # key declaring `columns:`.
12
+ class InvalidVariablesError < Error; end
13
+ end