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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Controller for managing payments within quotes
5
+ # Provides nested resource CRUD operations for payment tracking
6
+ class PaymentsController < ApplicationController
7
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
8
+
9
+ before_action :set_quote
10
+ before_action :set_payment, only: %i[show update destroy]
11
+
12
+ # GET /budget/quotes/:quote_id/payments
13
+ def index
14
+ @payments = @quote.payments.ordered
15
+ render json: @payments.map { |payment|
16
+ {
17
+ id: payment.id,
18
+ amount: payment.amount,
19
+ payment_date: payment.payment_date,
20
+ payment_method: payment.payment_method,
21
+ notes: payment.notes,
22
+ created_at: payment.created_at,
23
+ payment_method_name: payment.payment_method_name
24
+ }
25
+ }
26
+ end
27
+
28
+ # GET /budget/quotes/:quote_id/payments/:id
29
+ def show
30
+ # @payment set by before_action
31
+ render json: {
32
+ id: @payment.id,
33
+ amount: @payment.amount,
34
+ payment_date: @payment.payment_date,
35
+ payment_method: @payment.payment_method,
36
+ notes: @payment.notes,
37
+ created_at: @payment.created_at,
38
+ updated_at: @payment.updated_at,
39
+ payment_method_name: @payment.payment_method_name,
40
+ quote_id: @payment.budget_quote_id
41
+ }
42
+ end
43
+
44
+ # POST /budget/quotes/:quote_id/payments
45
+ def create
46
+ @payment = @quote.payments.new(payment_params)
47
+
48
+ if @payment.save
49
+ render json: {
50
+ id: @payment.id,
51
+ amount: @payment.amount,
52
+ payment_date: @payment.payment_date,
53
+ payment_method: @payment.payment_method,
54
+ notes: @payment.notes,
55
+ created_at: @payment.created_at,
56
+ updated_at: @payment.updated_at,
57
+ payment_method_name: @payment.payment_method_name,
58
+ quote_id: @payment.budget_quote_id
59
+ }, status: :created
60
+ else
61
+ render json: { errors: @payment.errors.full_messages }, status: :unprocessable_entity
62
+ end
63
+ end
64
+
65
+ # PATCH/PUT /budget/quotes/:quote_id/payments/:id
66
+ def update
67
+ if @payment.update(payment_params)
68
+ render json: {
69
+ id: @payment.id,
70
+ amount: @payment.amount,
71
+ payment_date: @payment.payment_date,
72
+ payment_method: @payment.payment_method,
73
+ notes: @payment.notes,
74
+ created_at: @payment.created_at,
75
+ updated_at: @payment.updated_at,
76
+ payment_method_name: @payment.payment_method_name,
77
+ quote_id: @payment.budget_quote_id
78
+ }
79
+ else
80
+ render json: { errors: @payment.errors.full_messages }, status: :unprocessable_entity
81
+ end
82
+ end
83
+
84
+ # DELETE /budget/quotes/:quote_id/payments/:id
85
+ def destroy
86
+ @payment.destroy
87
+ head :no_content
88
+ end
89
+
90
+ private
91
+
92
+ def set_quote
93
+ @quote = Quote.find(params[:quote_id])
94
+ end
95
+
96
+ def set_payment
97
+ @payment = @quote.payments.find(params[:id])
98
+ end
99
+
100
+ def payment_params
101
+ params.require(:payment).permit(:amount, :payment_date, :payment_method, :notes)
102
+ end
103
+
104
+ def not_found
105
+ render json: { error: 'Not found' }, status: :not_found
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # Controller for managing budget quotes via REST API
5
+ # Provides CRUD operations and summary endpoints for quotes
6
+ class QuotesController < ApplicationController
7
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
8
+
9
+ before_action :set_quote, only: %i[show update destroy]
10
+
11
+ # GET /budget/quotes
12
+ def index
13
+ @quotes = Quote.includes(:line_items, :payments)
14
+ .order(created_at: :desc)
15
+
16
+ render json: @quotes.map { |quote|
17
+ {
18
+ id: quote.id,
19
+ customer_name: quote.customer_name,
20
+ customer_contact: quote.customer_contact,
21
+ quote_date: quote.quote_date,
22
+ created_at: quote.created_at,
23
+ line_items_count: quote.line_items.size,
24
+ payments_count: quote.payments.size,
25
+ totals: {
26
+ total: quote.total,
27
+ total_paid: quote.total_paid,
28
+ remaining_balance: quote.remaining_balance,
29
+ fully_paid: quote.fully_paid?
30
+ }
31
+ }
32
+ }
33
+ end
34
+
35
+ # GET /budget/quotes/:id
36
+ def show
37
+ # @quote set by before_action
38
+ render json: {
39
+ id: @quote.id,
40
+ customer_name: @quote.customer_name,
41
+ customer_contact: @quote.customer_contact,
42
+ notes: @quote.notes,
43
+ quote_date: @quote.quote_date,
44
+ created_at: @quote.created_at,
45
+ updated_at: @quote.updated_at,
46
+ line_items: @quote.line_items.map do |item|
47
+ {
48
+ id: item.id,
49
+ description: item.description,
50
+ price: item.price,
51
+ category: item.category,
52
+ quantity: item.quantity,
53
+ created_at: item.created_at,
54
+ category_name: item.category_name,
55
+ subtotal: item.subtotal
56
+ }
57
+ end,
58
+ payments: @quote.payments.ordered.map do |payment|
59
+ {
60
+ id: payment.id,
61
+ amount: payment.amount,
62
+ payment_date: payment.payment_date,
63
+ payment_method: payment.payment_method,
64
+ notes: payment.notes,
65
+ created_at: payment.created_at,
66
+ payment_method_name: payment.payment_method_name
67
+ }
68
+ end,
69
+ totals: {
70
+ total: @quote.total,
71
+ total_paid: @quote.total_paid,
72
+ remaining_balance: @quote.remaining_balance,
73
+ fully_paid: @quote.fully_paid?
74
+ },
75
+ category_breakdown: @quote.category_breakdown
76
+ }
77
+ end
78
+
79
+ # POST /budget/quotes
80
+ def create
81
+ @quote = Quote.new(quote_params)
82
+
83
+ if @quote.save
84
+ render json: {
85
+ id: @quote.id,
86
+ customer_name: @quote.customer_name,
87
+ customer_contact: @quote.customer_contact,
88
+ notes: @quote.notes,
89
+ quote_date: @quote.quote_date,
90
+ created_at: @quote.created_at,
91
+ updated_at: @quote.updated_at,
92
+ line_items: [],
93
+ payments: [],
94
+ totals: {
95
+ total: 0.0,
96
+ total_paid: 0.0,
97
+ remaining_balance: 0.0,
98
+ fully_paid: false
99
+ },
100
+ category_breakdown: {}
101
+ }, status: :created
102
+ else
103
+ render json: { errors: @quote.errors.full_messages }, status: :unprocessable_entity
104
+ end
105
+ end
106
+
107
+ # PATCH/PUT /budget/quotes/:id
108
+ def update
109
+ if @quote.update(quote_params)
110
+ render json: {
111
+ id: @quote.id,
112
+ customer_name: @quote.customer_name,
113
+ customer_contact: @quote.customer_contact,
114
+ notes: @quote.notes,
115
+ quote_date: @quote.quote_date,
116
+ created_at: @quote.created_at,
117
+ updated_at: @quote.updated_at,
118
+ line_items: @quote.line_items.map do |item|
119
+ {
120
+ id: item.id,
121
+ description: item.description,
122
+ price: item.price,
123
+ category: item.category,
124
+ quantity: item.quantity,
125
+ created_at: item.created_at,
126
+ category_name: item.category_name,
127
+ subtotal: item.subtotal
128
+ }
129
+ end,
130
+ payments: @quote.payments.ordered.map do |payment|
131
+ {
132
+ id: payment.id,
133
+ amount: payment.amount,
134
+ payment_date: payment.payment_date,
135
+ payment_method: payment.payment_method,
136
+ notes: payment.notes,
137
+ created_at: payment.created_at,
138
+ payment_method_name: payment.payment_method_name
139
+ }
140
+ end,
141
+ totals: {
142
+ total: @quote.total,
143
+ total_paid: @quote.total_paid,
144
+ remaining_balance: @quote.remaining_balance,
145
+ fully_paid: @quote.fully_paid?
146
+ },
147
+ category_breakdown: @quote.category_breakdown
148
+ }
149
+ else
150
+ render json: { errors: @quote.errors.full_messages }, status: :unprocessable_entity
151
+ end
152
+ end
153
+
154
+ # DELETE /budget/quotes/:id
155
+ def destroy
156
+ @quote.destroy
157
+ head :no_content
158
+ end
159
+
160
+ # GET /budget/quotes/:id/summary
161
+ def summary
162
+ @quote = Quote.find(params[:id])
163
+ render json: @quote.summary
164
+ end
165
+
166
+ private
167
+
168
+ def set_quote
169
+ @quote = Quote.includes(:line_items, :payments).find(params[:id])
170
+ end
171
+
172
+ def quote_params
173
+ params.require(:quote).permit(:customer_name, :customer_contact, :quote_date, :notes)
174
+ end
175
+
176
+ def not_found
177
+ render json: { error: 'Not found' }, status: :not_found
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # ActiveRecord model for line items within quotes
5
+ # Represents individual items/services in a quote
6
+ class LineItem < ActiveRecord::Base
7
+ self.table_name = 'budget_line_items'
8
+
9
+ CATEGORIES = %w[lente montura tratamiento accesorio servicio other].freeze
10
+
11
+ # Associations
12
+ belongs_to :quote, class_name: 'Budget::Quote', foreign_key: 'budget_quote_id'
13
+
14
+ # Validations
15
+ validates :description, presence: true
16
+ validates :price, presence: true, numericality: { greater_than: 0 }
17
+ validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
18
+ validates :category, presence: true, inclusion: { in: CATEGORIES }
19
+
20
+ # Callbacks
21
+ before_validation :set_default_category
22
+
23
+ # Calculate subtotal (price * quantity)
24
+ # @return [Float] Subtotal
25
+ def subtotal
26
+ price * quantity
27
+ end
28
+
29
+ # Get category in Spanish
30
+ # @return [String] Category name in Spanish
31
+ def category_name
32
+ {
33
+ 'lente' => 'Lente',
34
+ 'montura' => 'Montura',
35
+ 'tratamiento' => 'Tratamiento',
36
+ 'accesorio' => 'Accesorio',
37
+ 'servicio' => 'Servicio',
38
+ 'other' => 'Otro'
39
+ }[category] || category.to_s.capitalize
40
+ end
41
+
42
+ # Format line item for display
43
+ # @return [String] Formatted line item
44
+ def to_s
45
+ if quantity > 1
46
+ "#{category_name} - #{description} (#{quantity} x $#{format('%.2f', price)}) = $#{format('%.2f', subtotal)}"
47
+ else
48
+ "#{category_name} - #{description}: $#{format('%.2f', price)}"
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def set_default_category
55
+ self.category = 'other' if category.blank?
56
+ self.category = 'other' unless CATEGORIES.include?(category)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # ActiveRecord model for payments within quotes
5
+ # Tracks payment transactions for quotes
6
+ class Payment < ActiveRecord::Base
7
+ self.table_name = 'budget_payments'
8
+
9
+ PAYMENT_METHODS = %w[efectivo tarjeta transferencia cheque other].freeze
10
+
11
+ # Associations
12
+ belongs_to :quote, class_name: 'Budget::Quote', foreign_key: 'budget_quote_id'
13
+
14
+ # Validations
15
+ validates :amount, presence: true, numericality: { greater_than: 0 }
16
+ validates :payment_date, presence: true
17
+ validates :payment_method, presence: true, inclusion: { in: PAYMENT_METHODS }
18
+
19
+ # Callbacks
20
+ before_validation :set_default_payment_method
21
+ before_validation :set_default_payment_date
22
+
23
+ # Scopes
24
+ scope :ordered, -> { order(:payment_date) }
25
+ scope :by_method, ->(method) { where(payment_method: method) }
26
+
27
+ # Get payment method in Spanish
28
+ # @return [String] Payment method in Spanish
29
+ def payment_method_name
30
+ {
31
+ 'efectivo' => 'Efectivo',
32
+ 'tarjeta' => 'Tarjeta',
33
+ 'transferencia' => 'Transferencia',
34
+ 'cheque' => 'Cheque',
35
+ 'other' => 'Otro'
36
+ }[payment_method] || payment_method.to_s.capitalize
37
+ end
38
+
39
+ # Format payment for display
40
+ # @return [String] Formatted payment
41
+ def to_s
42
+ output = "$#{format('%.2f', amount)}, #{payment_method_name} (#{payment_date.strftime('%d/%m/%Y')})"
43
+ output += " - #{notes}" if notes.present?
44
+ output
45
+ end
46
+
47
+ private
48
+
49
+ def set_default_payment_method
50
+ self.payment_method = 'efectivo' if payment_method.blank?
51
+ self.payment_method = 'other' unless PAYMENT_METHODS.include?(payment_method)
52
+ end
53
+
54
+ def set_default_payment_date
55
+ self.payment_date ||= Time.current
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Budget
4
+ # ActiveRecord model for budget quotes
5
+ # Manages quotes with line items and payments
6
+ class Quote < ActiveRecord::Base
7
+ self.table_name = 'budget_quotes'
8
+
9
+ # External associations (optional - can be defined in host application)
10
+ # Uncomment this line in your host app's Budget::Quote decorator if needed:
11
+ # belongs_to :prescription, optional: true
12
+
13
+ # Associations
14
+ has_many :line_items, class_name: 'Budget::LineItem', foreign_key: 'budget_quote_id', dependent: :destroy
15
+ has_many :payments, class_name: 'Budget::Payment', foreign_key: 'budget_quote_id', dependent: :destroy
16
+
17
+ # Validations
18
+ validates :customer_name, presence: true
19
+ validates :quote_date, presence: true
20
+
21
+ # Callbacks
22
+ before_validation :set_quote_date, on: :create
23
+
24
+ # Scopes
25
+ scope :recent, -> { order(created_at: :desc) }
26
+ scope :by_customer, ->(name) { where('customer_name LIKE ?', "%#{name}%") }
27
+ scope :pending, lambda {
28
+ # Find quotes where payments don't cover all line items
29
+ # This requires calculating totals in Ruby since we don't have a total column
30
+ all.reject(&:fully_paid?)
31
+ }
32
+
33
+ # Calculate total price of all line items
34
+ # @return [Float] Total price
35
+ def total
36
+ line_items.sum(&:subtotal)
37
+ end
38
+
39
+ # Calculate total amount paid so far
40
+ # @return [Float] Total paid
41
+ def total_paid
42
+ payments.sum(:amount)
43
+ end
44
+
45
+ # Calculate remaining balance to be paid
46
+ # @return [Float] Remaining amount
47
+ def remaining_balance
48
+ total - total_paid
49
+ end
50
+
51
+ # Check if quote is fully paid
52
+ # @return [Boolean]
53
+ def fully_paid?
54
+ remaining_balance <= 0
55
+ end
56
+
57
+ # Get the initial payment (adelanto)
58
+ # @return [Payment, nil] First payment or nil if no payments
59
+ def initial_payment
60
+ payments.order(:payment_date).first
61
+ end
62
+
63
+ # Get breakdown by category
64
+ # @return [Hash] Category totals
65
+ def category_breakdown
66
+ breakdown = Hash.new(0)
67
+ line_items.each do |item|
68
+ breakdown[item.category.to_sym] += item.subtotal
69
+ end
70
+ breakdown
71
+ end
72
+
73
+ # Generate a summary of the quote
74
+ # @return [Hash] Quote summary
75
+ def summary
76
+ {
77
+ id: id,
78
+ customer_name: customer_name,
79
+ customer_contact: customer_contact,
80
+ date: quote_date,
81
+ line_items_count: line_items.count,
82
+ total: total,
83
+ total_paid: total_paid,
84
+ remaining_balance: remaining_balance,
85
+ fully_paid: fully_paid?,
86
+ category_breakdown: category_breakdown,
87
+ payments_count: payments.count
88
+ }
89
+ end
90
+
91
+ # Format quote for display
92
+ # @return [String] Formatted quote
93
+ def to_s
94
+ output = []
95
+ output << ('=' * 60)
96
+ output << "PRESUPUESTO ##{id}"
97
+ output << ('=' * 60)
98
+ output << "Cliente: #{customer_name}"
99
+ output << "Contacto: #{customer_contact}" if customer_contact
100
+ output << "Fecha: #{quote_date.strftime('%d/%m/%Y')}"
101
+ output << "Notas: #{notes}" if notes
102
+ output << ''
103
+ output << 'DETALLE:'
104
+ output << ('-' * 60)
105
+
106
+ line_items.each_with_index do |item, index|
107
+ output << "#{index + 1}. #{item}"
108
+ end
109
+
110
+ output << ('-' * 60)
111
+ output << "TOTAL: $#{format('%.2f', total)}"
112
+ output << ''
113
+
114
+ if payments.any?
115
+ output << 'PAGOS:'
116
+ output << ('-' * 60)
117
+ payments.order(:payment_date).each_with_index do |payment, index|
118
+ output << "#{index + 1}. #{payment}"
119
+ end
120
+ output << ('-' * 60)
121
+ output << "Total Pagado: $#{format('%.2f', total_paid)}"
122
+ end
123
+
124
+ output << ''
125
+ output << "SALDO PENDIENTE: $#{format('%.2f', remaining_balance)}"
126
+ output << "Estado: #{fully_paid? ? 'PAGADO COMPLETO' : 'PENDIENTE'}"
127
+ output << ('=' * 60)
128
+
129
+ output.join("\n")
130
+ end
131
+
132
+ # Add a line item to the quote
133
+ def add_line_item(description:, price:, category: 'other', quantity: 1)
134
+ line_items.create!(
135
+ description: description,
136
+ price: price,
137
+ category: category,
138
+ quantity: quantity
139
+ )
140
+ end
141
+
142
+ # Add a payment (adelanto or subsequent payment)
143
+ def add_payment(amount:, payment_date: nil, payment_method: 'efectivo', notes: nil)
144
+ payments.create!(
145
+ amount: amount,
146
+ payment_date: payment_date || Time.current,
147
+ payment_method: payment_method,
148
+ notes: notes
149
+ )
150
+ end
151
+
152
+ private
153
+
154
+ def set_quote_date
155
+ self.quote_date ||= Time.current
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array! @line_items do |item|
4
+ json.extract! item, :id, :description, :price, :category, :quantity, :created_at
5
+ json.category_name item.category_name
6
+ json.subtotal item.subtotal
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.extract! @line_item, :id, :description, :price, :category, :quantity, :created_at, :updated_at
4
+ json.category_name @line_item.category_name
5
+ json.subtotal @line_item.subtotal
6
+ json.quote_id @line_item.budget_quote_id
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array! @payments do |payment|
4
+ json.extract! payment, :id, :amount, :payment_date, :payment_method, :notes, :created_at
5
+ json.payment_method_name payment.payment_method_name
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.extract! @payment, :id, :amount, :payment_date, :payment_method, :notes, :created_at, :updated_at
4
+ json.payment_method_name @payment.payment_method_name
5
+ json.quote_id @payment.budget_quote_id
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.array! @quotes do |quote|
4
+ json.extract! quote, :id, :customer_name, :customer_contact, :quote_date, :created_at
5
+
6
+ json.line_items_count quote.line_items.size
7
+ json.payments_count quote.payments.size
8
+
9
+ json.totals do
10
+ json.total quote.total
11
+ json.total_paid quote.total_paid
12
+ json.remaining_balance quote.remaining_balance
13
+ json.fully_paid quote.fully_paid?
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.extract! @quote, :id, :customer_name, :customer_contact, :notes, :quote_date, :created_at, :updated_at
4
+
5
+ json.line_items @quote.line_items do |item|
6
+ json.extract! item, :id, :description, :price, :category, :quantity, :created_at
7
+ json.category_name item.category_name
8
+ json.subtotal item.subtotal
9
+ end
10
+
11
+ json.payments @quote.payments.ordered do |payment|
12
+ json.extract! payment, :id, :amount, :payment_date, :payment_method, :notes, :created_at
13
+ json.payment_method_name payment.payment_method_name
14
+ end
15
+
16
+ json.totals do
17
+ json.total @quote.total
18
+ json.total_paid @quote.total_paid
19
+ json.remaining_balance @quote.remaining_balance
20
+ json.fully_paid @quote.fully_paid?
21
+ end
22
+
23
+ json.category_breakdown @quote.category_breakdown
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Budget::Engine.routes.draw do
4
+ resources :quotes do
5
+ member do
6
+ get :summary
7
+ end
8
+
9
+ resources :line_items, only: %i[index show create update destroy]
10
+ resources :payments, only: %i[index show create update destroy]
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBudgetQuotes < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :budget_quotes do |t|
6
+ t.string :customer_name, null: false
7
+ t.string :customer_contact
8
+ t.text :notes
9
+ t.datetime :quote_date
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :budget_quotes, :customer_name
15
+ add_index :budget_quotes, :created_at
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBudgetLineItems < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :budget_line_items do |t|
6
+ t.references :budget_quote, null: false, foreign_key: true, index: true
7
+ t.string :description, null: false
8
+ t.decimal :price, precision: 10, scale: 2, null: false, default: 0.0
9
+ t.string :category, default: 'other'
10
+ t.integer :quantity, default: 1, null: false
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :budget_line_items, :category
16
+ end
17
+ end