lbyte-budget 0.1.1

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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.simplecov +25 -0
  3. data/API_DOCUMENTATION.md +526 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CLEANUP.md +85 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/INTEGRATION_GUIDE.md +452 -0
  8. data/LICENSE.txt +21 -0
  9. data/RAILS_USAGE.md +510 -0
  10. data/README.md +367 -0
  11. data/Rakefile +19 -0
  12. data/TESTING.md +243 -0
  13. data/TEST_IMPLEMENTATION_SUMMARY.md +246 -0
  14. data/TEST_SUITE.md +253 -0
  15. data/app/controllers/budget/application_controller.rb +10 -0
  16. data/app/controllers/budget/line_items_controller.rb +112 -0
  17. data/app/controllers/budget/payments_controller.rb +108 -0
  18. data/app/controllers/budget/quotes_controller.rb +180 -0
  19. data/app/models/budget/line_item.rb +59 -0
  20. data/app/models/budget/payment.rb +58 -0
  21. data/app/models/budget/quote.rb +158 -0
  22. data/app/views/budget/line_items/index.json.jbuilder +7 -0
  23. data/app/views/budget/line_items/show.json.jbuilder +6 -0
  24. data/app/views/budget/payments/index.json.jbuilder +6 -0
  25. data/app/views/budget/payments/show.json.jbuilder +5 -0
  26. data/app/views/budget/quotes/index.json.jbuilder +15 -0
  27. data/app/views/budget/quotes/show.json.jbuilder +23 -0
  28. data/config/routes.rb +12 -0
  29. data/db/migrate/20251129000001_create_budget_quotes.rb +17 -0
  30. data/db/migrate/20251129000002_create_budget_line_items.rb +17 -0
  31. data/db/migrate/20251129000003_create_budget_payments.rb +18 -0
  32. data/examples/basic_usage.rb +130 -0
  33. data/examples/test_examples.rb +113 -0
  34. data/lib/budget/engine.rb +25 -0
  35. data/lib/budget/line_item.rb +66 -0
  36. data/lib/budget/payment.rb +56 -0
  37. data/lib/budget/quote.rb +163 -0
  38. data/lib/budget/version.rb +5 -0
  39. data/lib/budget.rb +10 -0
  40. data/lib/generators/budget/INSTALL +46 -0
  41. data/lib/generators/budget/install_generator.rb +33 -0
  42. data/sig/budget.rbs +4 -0
  43. metadata +115 -0
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBudgetPayments < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :budget_payments do |t|
6
+ t.references :budget_quote, null: false, foreign_key: true, index: true
7
+ t.decimal :amount, precision: 10, scale: 2, null: false, default: 0.0
8
+ t.datetime :payment_date, null: false
9
+ t.string :payment_method, default: 'efectivo'
10
+ t.text :notes
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :budget_payments, :payment_date
16
+ add_index :budget_payments, :payment_method
17
+ end
18
+ end
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This example requires a Rails environment with the Budget engine loaded
5
+ # Run from a Rails app that has the budget gem installed:
6
+ # rails runner examples/basic_usage.rb
7
+
8
+ puts '=' * 70
9
+ puts 'EJEMPLO DE USO DEL SISTEMA DE PRESUPUESTOS'
10
+ puts '=' * 70
11
+ puts
12
+
13
+ # 1. Create a new quote for a customer
14
+ puts '1. Creando un nuevo presupuesto para María González...'
15
+ quote = Budget::Quote.create!(
16
+ customer_name: 'María González',
17
+ customer_contact: '555-1234',
18
+ notes: 'Prefiere montura liviana'
19
+ )
20
+ puts "✓ Presupuesto ##{quote.id} creado"
21
+ puts
22
+
23
+ # 2. Add line items (lentes, montura, tratamiento)
24
+ puts '2. Agregando artículos al presupuesto...'
25
+
26
+ # Add lenses
27
+ quote.add_line_item(
28
+ description: 'Lentes progresivos alta gama',
29
+ price: 150.00,
30
+ category: 'lente'
31
+ )
32
+ puts '✓ Agregado: Lentes progresivos - $150.00'
33
+
34
+ # Add frame
35
+ quote.add_line_item(
36
+ description: 'Montura de titanio ligera',
37
+ price: 80.00,
38
+ category: 'montura'
39
+ )
40
+ puts '✓ Agregado: Montura de titanio - $80.00'
41
+
42
+ # Add treatment
43
+ quote.add_line_item(
44
+ description: 'Tratamiento anti-reflejante',
45
+ price: 35.00,
46
+ category: 'tratamiento'
47
+ )
48
+ puts '✓ Agregado: Tratamiento anti-reflejante - $35.00'
49
+
50
+ # Add additional treatment
51
+ quote.add_line_item(
52
+ description: 'Protección UV',
53
+ price: 25.00,
54
+ category: 'tratamiento'
55
+ )
56
+ puts '✓ Agregado: Protección UV - $25.00'
57
+
58
+ # Add case
59
+ quote.add_line_item(
60
+ description: 'Estuche premium',
61
+ price: 10.00,
62
+ category: 'other'
63
+ )
64
+ puts '✓ Agregado: Estuche premium - $10.00'
65
+ puts
66
+
67
+ # 3. Display quote
68
+ puts '3. Presupuesto completo:'
69
+ puts quote
70
+ puts
71
+
72
+ # 4. Add initial payment (adelanto)
73
+ puts '4. Cliente realiza adelanto del 50%...'
74
+ adelanto_amount = quote.total * 0.5
75
+ quote.add_payment(
76
+ amount: adelanto_amount,
77
+ payment_method: 'efectivo',
78
+ notes: 'Adelanto inicial (50%)'
79
+ )
80
+ puts "✓ Adelanto de $#{format('%.2f', adelanto_amount)} registrado"
81
+ puts " Saldo pendiente: $#{format('%.2f', quote.remaining_balance)}"
82
+ puts
83
+
84
+ # 5. Display updated quote
85
+ puts '5. Presupuesto actualizado:'
86
+ puts quote
87
+ puts
88
+
89
+ # Simulate time passing and customer returning
90
+ puts '=' * 70
91
+ puts '... 2 semanas después ...'
92
+ puts '=' * 70
93
+ puts
94
+
95
+ # 6. Customer returns to complete payment
96
+ puts '6. Cliente regresa para completar el pago...'
97
+ remaining = quote.remaining_balance
98
+ quote.add_payment(
99
+ amount: remaining,
100
+ payment_method: 'tarjeta',
101
+ notes: 'Pago final - retira lentes'
102
+ )
103
+ puts "✓ Pago final de $#{format('%.2f', remaining)} registrado"
104
+ puts "✓ Estado: #{quote.fully_paid? ? 'PAGADO COMPLETO ✓' : 'PENDIENTE'}"
105
+ puts
106
+
107
+ # 7. Display final quote
108
+ puts '7. Presupuesto final:'
109
+ puts quote
110
+ puts
111
+
112
+ # 8. Display summary
113
+ puts '8. Resumen del presupuesto:'
114
+ summary = quote.summary
115
+ puts " ID: #{summary[:id]}"
116
+ puts " Cliente: #{summary[:customer_name]}"
117
+ puts " Total: $#{format('%.2f', summary[:total])}"
118
+ puts " Pagado: $#{format('%.2f', summary[:total_paid])}"
119
+ puts " Pendiente: $#{format('%.2f', summary[:remaining_balance])}"
120
+ puts " Estado: #{summary[:fully_paid] ? 'COMPLETO' : 'PENDIENTE'}"
121
+ puts
122
+ puts ' Desglose por categoría:'
123
+ summary[:category_breakdown].each do |category, amount|
124
+ puts " - #{category}: $#{format('%.2f', amount)}"
125
+ end
126
+ puts
127
+
128
+ puts '=' * 70
129
+ puts 'EJEMPLO COMPLETADO'
130
+ puts '=' * 70
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Quick Test Examples - Shows what the test suite validates
5
+ # Run from a Rails app that has the budget gem installed:
6
+ # rails runner examples/test_examples.rb
7
+
8
+ puts '=' * 70
9
+ puts 'BUDGET GEM - TEST EXAMPLES'
10
+ puts '=' * 70
11
+ puts
12
+
13
+ # Example 1: LineItem validation
14
+ puts '1. LineItem - Category Validation'
15
+ puts '-' * 70
16
+ quote = Budget::Quote.create!(customer_name: 'Test Customer')
17
+ item1 = quote.line_items.create!(description: 'Lente', price: 100, category: 'lente')
18
+ item2 = quote.line_items.build(description: 'Invalid', price: 50, category: 'invalid_cat')
19
+ item2.valid? # This will fail validation
20
+ puts "Valid category ('lente'): #{item1.category}"
21
+ puts "Invalid category validation: #{item2.errors[:category].join(', ')}" if item2.errors[:category].any?
22
+ puts
23
+
24
+ # Example 2: Payment method validation
25
+ puts '2. Payment - Method Validation'
26
+ puts '-' * 70
27
+ payment1 = quote.payments.create!(amount: 100, payment_method: 'tarjeta')
28
+ payment2 = quote.payments.create!(amount: 50, payment_method: 'efectivo')
29
+ puts "tarjeta → #{payment1.payment_method}"
30
+ puts "efectivo → #{payment2.payment_method}"
31
+ puts
32
+
33
+ # Example 3: Quote calculations
34
+ puts '3. Quote - Total Calculations'
35
+ puts '-' * 70
36
+ calc_quote = Budget::Quote.create!(customer_name: 'Test Customer')
37
+ calc_quote.add_line_item(description: 'Lente', price: 150.0, category: 'lente')
38
+ calc_quote.add_line_item(description: 'Montura', price: 80.0, category: 'montura')
39
+ calc_quote.add_line_item(description: 'Tratamiento', price: 35.0, category: 'tratamiento', quantity: 2)
40
+ puts 'Items: Lente ($150) + Montura ($80) + Tratamiento ($35 x 2)'
41
+ puts "Total: $#{calc_quote.total}"
42
+ puts "Category breakdown: #{calc_quote.category_breakdown}"
43
+ puts
44
+
45
+ # Example 4: Payment tracking
46
+ puts '4. Quote - Payment Tracking'
47
+ puts '-' * 70
48
+ calc_quote.add_payment(amount: 150.0, notes: 'Adelanto (50%)')
49
+ puts "After adelanto: Paid=$#{calc_quote.total_paid}, Remaining=$#{calc_quote.remaining_balance}"
50
+ calc_quote.add_payment(amount: calc_quote.remaining_balance, notes: 'Final payment')
51
+ puts "After final payment: Paid=$#{calc_quote.total_paid}, Remaining=$#{calc_quote.remaining_balance}"
52
+ puts "Fully paid? #{quote.fully_paid?}"
53
+ puts
54
+
55
+ # Example 5: Type conversions
56
+ puts '5. Type Conversions'
57
+ puts '-' * 70
58
+ item = Budget::LineItem.new(description: 'Test', price: '99.99', quantity: '3')
59
+ puts "String price '99.99' → #{item.price.class}: #{item.price}"
60
+ puts "String quantity '3' → #{item.quantity.class}: #{item.quantity}"
61
+ puts "Subtotal: $#{item.subtotal}"
62
+ puts
63
+
64
+ # Example 6: Edge cases
65
+ puts '6. Edge Cases'
66
+ puts '-' * 70
67
+ edge_quote = Budget.create_quote(customer_name: 'Edge Test')
68
+ edge_quote.add_line_item(description: 'Item', price: 100.0)
69
+ edge_quote.add_payment(amount: 150.0) # Overpayment
70
+ puts 'Total: $100, Paid: $150'
71
+ puts "Remaining balance (overpayment): $#{edge_quote.remaining_balance}"
72
+ puts "Fully paid? #{edge_quote.fully_paid?}"
73
+ puts
74
+
75
+ # Example 7: Spanish formatting
76
+ puts '7. Spanish Translations'
77
+ puts '-' * 70
78
+ categories = %i[lente montura tratamiento other]
79
+ categories.each do |cat|
80
+ item = Budget::LineItem.new(description: 'Test', price: 50, category: cat)
81
+ puts "#{cat} → #{item.category_name}"
82
+ end
83
+ puts
84
+ payment_methods = %w[efectivo tarjeta transferencia cheque other]
85
+ payment_methods.each do |method|
86
+ payment = Budget::Payment.new(amount: 50, payment_method: method)
87
+ puts "#{method} → #{payment.payment_method_name}"
88
+ end
89
+ puts
90
+
91
+ # Example 8: Complete workflow
92
+ puts '8. Complete Workflow (Integration Test)'
93
+ puts '-' * 70
94
+ complete_quote = Budget.create_quote(
95
+ customer_name: 'María González',
96
+ customer_contact: '555-1234'
97
+ )
98
+ complete_quote.add_line_item(description: 'Lentes progresivos', price: 150.0, category: :lente)
99
+ complete_quote.add_line_item(description: 'Montura titanio', price: 80.0, category: :montura)
100
+ complete_quote.add_line_item(description: 'Anti-reflejante', price: 35.0, category: :tratamiento)
101
+
102
+ puts "Total: $#{complete_quote.total}"
103
+ adelanto = complete_quote.total * 0.5
104
+ complete_quote.add_payment(amount: adelanto, notes: 'Adelanto 50%')
105
+ puts "After 50% adelanto: Remaining=$#{complete_quote.remaining_balance}"
106
+ complete_quote.add_payment(amount: complete_quote.remaining_balance, notes: 'Pago final')
107
+ puts "After final payment: Status=#{complete_quote.fully_paid? ? 'COMPLETO' : 'PENDIENTE'}"
108
+ puts
109
+
110
+ puts '=' * 70
111
+ puts 'All examples demonstrate test coverage scenarios'
112
+ puts "Run 'bundle exec rspec' to execute the full test suite"
113
+ puts '=' * 70
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Rails Engine for Budget gem
5
+ # Provides mountable engine functionality with isolated namespace
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Budget
8
+
9
+ # Ensure app directory is in autoload paths
10
+ config.autoload_paths += %W[
11
+ #{root}/app/models
12
+ #{root}/app/controllers
13
+ ]
14
+
15
+ # Add views path for JBuilder templates
16
+ config.paths['app/views'] ||= []
17
+ config.paths['app/views'] << File.expand_path('../../app/views', __dir__)
18
+
19
+ config.generators do |g|
20
+ g.test_framework :rspec
21
+ g.fixture_replacement :factory_bot
22
+ g.factory_bot dir: 'spec/factories'
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Represents a single line item in a quote
5
+ # Examples: lente (lens), montura (frame), tratamiento (treatment)
6
+ class LineItem
7
+ attr_accessor :description, :price, :category, :quantity
8
+
9
+ CATEGORIES = %w[lente montura tratamiento accesorio servicio other].freeze
10
+
11
+ def initialize(description:, price:, category: 'other', quantity: 1)
12
+ @description = description
13
+ @price = price.to_f
14
+ @category = validate_category(category)
15
+ @quantity = quantity.to_i
16
+ end
17
+
18
+ # Calculate subtotal (price * quantity)
19
+ # @return [Float] Subtotal
20
+ def subtotal
21
+ @price * @quantity
22
+ end
23
+
24
+ # Get category in Spanish
25
+ # @return [String] Category name in Spanish
26
+ def category_name
27
+ {
28
+ 'lente' => 'Lente',
29
+ 'montura' => 'Montura',
30
+ 'tratamiento' => 'Tratamiento',
31
+ 'accesorio' => 'Accesorio',
32
+ 'servicio' => 'Servicio',
33
+ 'other' => 'Otro'
34
+ }[@category] || @category.capitalize
35
+ end
36
+
37
+ # Format line item for display
38
+ # @return [String] Formatted line item
39
+ def to_s
40
+ if @quantity > 1
41
+ "#{category_name} - #{@description} (#{@quantity} x $#{format('%.2f', @price)}) = $#{format('%.2f', subtotal)}"
42
+ else
43
+ "#{category_name} - #{@description}: $#{format('%.2f', @price)}"
44
+ end
45
+ end
46
+
47
+ # Convert to hash
48
+ # @return [Hash] Line item as hash
49
+ def to_h
50
+ {
51
+ description: @description,
52
+ price: @price,
53
+ category: @category,
54
+ quantity: @quantity,
55
+ subtotal: subtotal
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def validate_category(category)
62
+ category = category.to_s
63
+ CATEGORIES.include?(category) ? category : 'other'
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Represents a payment made towards a quote
5
+ # Can be initial payment (adelanto) or subsequent payments
6
+ class Payment
7
+ attr_accessor :amount, :payment_date, :payment_method, :notes
8
+
9
+ PAYMENT_METHODS = %w[efectivo tarjeta transferencia cheque other].freeze
10
+
11
+ def initialize(amount:, payment_date: nil, payment_method: 'efectivo', notes: nil)
12
+ @amount = amount.to_f
13
+ @payment_date = payment_date || Time.now
14
+ @payment_method = validate_payment_method(payment_method)
15
+ @notes = notes
16
+ end
17
+
18
+ # Get payment method in Spanish
19
+ # @return [String] Payment method in Spanish
20
+ def payment_method_name
21
+ {
22
+ 'efectivo' => 'Efectivo',
23
+ 'tarjeta' => 'Tarjeta',
24
+ 'transferencia' => 'Transferencia',
25
+ 'cheque' => 'Cheque',
26
+ 'other' => 'Otro'
27
+ }[@payment_method] || @payment_method.capitalize
28
+ end
29
+
30
+ # Format payment for display
31
+ # @return [String] Formatted payment
32
+ def to_s
33
+ output = "$#{format('%.2f', @amount)} - #{payment_method_name} (#{@payment_date.strftime('%d/%m/%Y')})"
34
+ output += " - #{@notes}" if @notes
35
+ output
36
+ end
37
+
38
+ # Convert to hash
39
+ # @return [Hash] Payment as hash
40
+ def to_h
41
+ {
42
+ amount: @amount,
43
+ payment_date: @payment_date,
44
+ payment_method: @payment_method,
45
+ notes: @notes
46
+ }
47
+ end
48
+
49
+ private
50
+
51
+ def validate_payment_method(method)
52
+ method = method.to_s.downcase
53
+ PAYMENT_METHODS.include?(method) ? method : 'other'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Represents a quote/budget for eyeglasses purchase
5
+ # Manages line items (lentes, montura, tratamiento) and payments
6
+ class Quote
7
+ attr_accessor :id, :customer_name, :customer_contact, :date, :notes
8
+ attr_reader :line_items, :payments
9
+
10
+ def initialize(customer_name:, id: nil, customer_contact: nil, date: nil, notes: nil)
11
+ @id = id || generate_id
12
+ @customer_name = customer_name
13
+ @customer_contact = customer_contact
14
+ @date = date || Time.now
15
+ @notes = notes
16
+ @line_items = []
17
+ @payments = []
18
+ end
19
+
20
+ # Add a line item to the quote
21
+ # @param description [String] Item description (e.g., "Lente progresivo", "Montura metal")
22
+ # @param price [Float] Item price
23
+ # @param category [Symbol] :lente, :montura, :tratamiento, :other
24
+ def add_line_item(description:, price:, category: :other, quantity: 1)
25
+ # Convert symbol to string for consistency
26
+ category = category.to_s if category.is_a?(Symbol)
27
+ line_item = LineItem.new(
28
+ description: description,
29
+ price: price,
30
+ category: category,
31
+ quantity: quantity
32
+ )
33
+ @line_items << line_item
34
+ line_item
35
+ end
36
+
37
+ # Remove a line item by index
38
+ def remove_line_item(index)
39
+ @line_items.delete_at(index)
40
+ end
41
+
42
+ # Add a payment (adelanto or subsequent payment)
43
+ # @param amount [Float] Payment amount
44
+ # @param payment_date [Time] When payment was made
45
+ # @param payment_method [String] How payment was made (efectivo, tarjeta, transferencia)
46
+ # @param notes [String] Additional notes about payment
47
+ def add_payment(amount:, payment_date: nil, payment_method: 'efectivo', notes: nil)
48
+ payment = Payment.new(
49
+ amount: amount,
50
+ payment_date: payment_date || Time.now,
51
+ payment_method: payment_method,
52
+ notes: notes
53
+ )
54
+ @payments << payment
55
+ payment
56
+ end
57
+
58
+ # Calculate total price of all line items
59
+ # @return [Float] Total price
60
+ def total
61
+ @line_items.sum(&:subtotal)
62
+ end
63
+
64
+ # Calculate total amount paid so far
65
+ # @return [Float] Total paid
66
+ def total_paid
67
+ @payments.sum(&:amount)
68
+ end
69
+
70
+ # Calculate remaining balance to be paid
71
+ # @return [Float] Remaining amount
72
+ def remaining_balance
73
+ total - total_paid
74
+ end
75
+
76
+ # Check if quote is fully paid
77
+ # @return [Boolean]
78
+ def fully_paid?
79
+ remaining_balance <= 0
80
+ end
81
+
82
+ # Get the initial payment (adelanto)
83
+ # @return [Payment, nil] First payment or nil if no payments
84
+ def initial_payment
85
+ @payments.first
86
+ end
87
+
88
+ # Get breakdown by category
89
+ # @return [Hash] Category totals
90
+ def category_breakdown
91
+ breakdown = Hash.new(0)
92
+ @line_items.each do |item|
93
+ breakdown[item.category] += item.subtotal
94
+ end
95
+ breakdown
96
+ end
97
+
98
+ # Generate a summary of the quote
99
+ # @return [Hash] Quote summary
100
+ def summary
101
+ {
102
+ id: @id,
103
+ customer_name: @customer_name,
104
+ customer_contact: @customer_contact,
105
+ date: @date,
106
+ line_items_count: @line_items.count,
107
+ total: total,
108
+ total_paid: total_paid,
109
+ remaining_balance: remaining_balance,
110
+ fully_paid: fully_paid?,
111
+ category_breakdown: category_breakdown,
112
+ payments_count: @payments.count
113
+ }
114
+ end
115
+
116
+ # Format quote for display
117
+ # @return [String] Formatted quote
118
+ def to_s
119
+ output = []
120
+ output << ('=' * 60)
121
+ output << "PRESUPUESTO ##{@id}"
122
+ output << ('=' * 60)
123
+ output << "Cliente: #{@customer_name}"
124
+ output << "Contacto: #{@customer_contact}" if @customer_contact
125
+ output << "Fecha: #{@date.strftime('%d/%m/%Y')}"
126
+ output << "Notas: #{@notes}" if @notes
127
+ output << ''
128
+ output << 'DETALLE:'
129
+ output << ('-' * 60)
130
+
131
+ @line_items.each_with_index do |item, index|
132
+ output << "#{index + 1}. #{item}"
133
+ end
134
+
135
+ output << ('-' * 60)
136
+ output << "TOTAL: $#{format('%.2f', total)}"
137
+ output << ''
138
+
139
+ if @payments.any?
140
+ output << 'PAGOS:'
141
+ output << ('-' * 60)
142
+ @payments.each_with_index do |payment, index|
143
+ output << "#{index + 1}. #{payment}"
144
+ end
145
+ output << ('-' * 60)
146
+ output << "Total Pagado: $#{format('%.2f', total_paid)}"
147
+ end
148
+
149
+ output << ''
150
+ output << "SALDO PENDIENTE: $#{format('%.2f', remaining_balance)}"
151
+ output << "Estado: #{fully_paid? ? 'PAGADO COMPLETO' : 'PENDIENTE'}"
152
+ output << ('=' * 60)
153
+
154
+ output.join("\n")
155
+ end
156
+
157
+ private
158
+
159
+ def generate_id
160
+ "Q#{Time.now.strftime('%Y%m%d%H%M%S')}"
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ VERSION = '0.1.1'
5
+ end
data/lib/budget.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'budget/version'
4
+ require_relative 'budget/engine'
5
+
6
+ # Budget module provides quote/budget management functionality
7
+ # Rails Engine for managing quotes with line items and payments
8
+ module Budget
9
+ class Error < StandardError; end
10
+ end
@@ -0,0 +1,46 @@
1
+ ===============================================================================
2
+
3
+ Budget Engine has been installed!
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migrations:
8
+
9
+ $ rails db:migrate
10
+
11
+ 2. Start using Budget in your Rails app:
12
+
13
+ # Create a new quote
14
+ quote = Budget::Quote.create!(
15
+ customer_name: "María González",
16
+ customer_contact: "555-1234"
17
+ )
18
+
19
+ # Add line items
20
+ quote.add_line_item(
21
+ description: "Lentes progresivos",
22
+ price: 150.00,
23
+ category: "lente"
24
+ )
25
+
26
+ # Add payment
27
+ quote.add_payment(
28
+ amount: 75.00,
29
+ payment_method: "efectivo",
30
+ notes: "Adelanto 50%"
31
+ )
32
+
33
+ # Check status
34
+ quote.total # => 150.0
35
+ quote.total_paid # => 75.0
36
+ quote.remaining_balance # => 75.0
37
+ quote.fully_paid? # => false
38
+
39
+ 3. View the quote:
40
+
41
+ puts quote # Formatted output
42
+
43
+ For more information, visit:
44
+ https://github.com/rubenpazch/lbyte-budget
45
+
46
+ ===============================================================================
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ module Budget
7
+ module Generators
8
+ # Rails generator for installing Budget migrations
9
+ # Creates migrations for quotes, line items, and payments tables
10
+ class InstallGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+
13
+ source_root File.expand_path('../../../db/migrate', __dir__)
14
+
15
+ desc 'Generates Budget migrations for quotes, line items, and payments'
16
+
17
+ def self.next_migration_number(dirname)
18
+ next_migration_number = current_migration_number(dirname) + 1
19
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
20
+ end
21
+
22
+ def copy_migrations
23
+ migration_template '20251129000001_create_budget_quotes.rb', 'db/migrate/create_budget_quotes.rb'
24
+ migration_template '20251129000002_create_budget_line_items.rb', 'db/migrate/create_budget_line_items.rb'
25
+ migration_template '20251129000003_create_budget_payments.rb', 'db/migrate/create_budget_payments.rb'
26
+ end
27
+
28
+ def show_readme
29
+ readme 'INSTALL' if behavior == :invoke
30
+ end
31
+ end
32
+ end
33
+ end
data/sig/budget.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Budget
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end