dscf-marketplace 0.2.92 → 0.2.94

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cf17e898ac996cc17f10dc5f2297f510d6f2c68e6f0c1d3430db98934106282
4
- data.tar.gz: 5efcbb36d3bfebfb462b92aeea5e386adf805da05f76ad10346e167a04dcf074
3
+ metadata.gz: b3d9e2b9c6d302c05aba856c7add2d572e6c48c763b29022c165c32fb0ae6d39
4
+ data.tar.gz: c1a4ed98de21d1ae80ce116f9db768e24e43baa4785896c9bcbb62bfcfb604e3
5
5
  SHA512:
6
- metadata.gz: cbf84769b2a1065d9e890f72c84ccd72732489ad1df9d8c1890df6602308c49545680d44eb2a2a89dc99fd737dcf8f42444fb78ee5c8b1b6f28fc5924f2355a2
7
- data.tar.gz: d7c9080cc4109ddd58dc1fb0aa701e8e93511b660079c47a9e2b5016718e513c00d67272c5755ab1717a8788b73f315510a7c228ff5a7a213a80799d743e8faf
6
+ metadata.gz: f88c9ecdd32d85066805cb205d3eb7e28df0f540d27ab6c9ecdfada565b3b068f3bb9ded7bdaae8c3d7f73581c4684633579da25b606c8460cf068c0075a26b3
7
+ data.tar.gz: ae10f6ddcdb722a28ecf61d00152984cbf20523bbf23fc33c385ea5378e1e22935304c636ff18e86e7f6fec2f33704def9a92087b43b33a71c809d5728660a57
@@ -2,7 +2,6 @@ module Dscf
2
2
  module Marketplace
3
3
  class OrdersController < ApplicationController
4
4
  include Dscf::Core::Common
5
- include Dscf::Marketplace::NestedCreatable
6
5
 
7
6
  def confirm
8
7
  @obj = find_record
@@ -61,10 +60,10 @@ module Dscf
61
60
 
62
61
  def default_serializer_includes
63
62
  {
64
- index: [ :user, :ordered_by, :ordered_to, :quotation, order_items: [:product] ],
63
+ index: [ :user, :ordered_by, :ordered_to, :quotation, order_items: [ :product ] ],
65
64
  show: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :delivery_order, :order_items ],
66
- create: [ :user, :ordered_by, :ordered_to, :quotation, :listing ],
67
- update: [ :user, :ordered_by, :ordered_to, :quotation, :listing ]
65
+ create: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :order_items ],
66
+ update: [ :user, :ordered_by, :ordered_to, :quotation, :listing, :order_items ]
68
67
  }
69
68
  end
70
69
  end
@@ -2,7 +2,6 @@ module Dscf
2
2
  module Marketplace
3
3
  class QuotationsController < ApplicationController
4
4
  include Dscf::Core::Common
5
- include Dscf::Marketplace::NestedCreatable
6
5
 
7
6
  def accept
8
7
  @obj = find_record
@@ -2,7 +2,6 @@ module Dscf
2
2
  module Marketplace
3
3
  class RequestForQuotationsController < ApplicationController
4
4
  include Dscf::Core::Common
5
- include Dscf::Marketplace::NestedCreatable
6
5
 
7
6
  def send_rfq
8
7
  @obj = find_record
@@ -34,6 +33,18 @@ module Dscf
34
33
  render_success("request_for_quotations.success.index", data: rfqs, serializer_options: options)
35
34
  end
36
35
 
36
+ def respond
37
+ @rfq = Dscf::Marketplace::RequestForQuotation.find(params[:id])
38
+ service = RfqResponseService.new(current_user)
39
+ result = service.respond_to_rfq(@rfq, params.permit(:notes, :valid_until, :delivery_date, quotation_items_attributes: [ :rfq_item_id, :unit_id, :quantity, :unit_price, :notes ]))
40
+
41
+ if result[:success]
42
+ render_success("request_for_quotations.success.responded", data: result[:quotation], serializer_options: {include: [ :business, quotation_items: [ :product, :unit, :rfq_item ] ]})
43
+ else
44
+ render_error("request_for_quotations.errors.respond_failed", errors: result[:errors])
45
+ end
46
+ end
47
+
37
48
  private
38
49
 
39
50
  def model_params
@@ -10,7 +10,7 @@ module Dscf::Marketplace
10
10
  belongs_to :ordered_by, class_name: "Dscf::Core::User"
11
11
  belongs_to :ordered_to, class_name: "Dscf::Core::Business"
12
12
  belongs_to :delivery_order, optional: true
13
- has_many :order_items, dependent: :destroy
13
+ has_many :order_items, dependent: :destroy, inverse_of: :order
14
14
  accepts_nested_attributes_for :order_items, allow_destroy: true
15
15
 
16
16
  validates :order_type, presence: true
@@ -4,7 +4,7 @@ module Dscf::Marketplace
4
4
 
5
5
  delegate :name, :description, :thumbnail_url, :images_urls, to: :product, allow_nil: true
6
6
 
7
- belongs_to :order, optional: true
7
+ belongs_to :order, optional: true, inverse_of: :order_items
8
8
  belongs_to :quotation_item, optional: true
9
9
  belongs_to :listing, optional: true
10
10
  belongs_to :product
@@ -4,7 +4,7 @@ module Dscf
4
4
  belongs_to :request_for_quotation, class_name: "Dscf::Marketplace::RequestForQuotation"
5
5
  belongs_to :business, class_name: "Dscf::Core::Business", optional: true if defined?(Dscf::Core)
6
6
 
7
- has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy
7
+ has_many :quotation_items, class_name: "Dscf::Marketplace::QuotationItem", dependent: :destroy, inverse_of: :quotation
8
8
  has_one :order, class_name: "Dscf::Marketplace::Order", dependent: :destroy
9
9
  accepts_nested_attributes_for :quotation_items, allow_destroy: true
10
10
 
@@ -61,18 +61,6 @@ module Dscf
61
61
  status == "draft"
62
62
  end
63
63
 
64
- def sent?
65
- status == "sent"
66
- end
67
-
68
- def accepted?
69
- status == "accepted"
70
- end
71
-
72
- def rejected?
73
- status == "rejected"
74
- end
75
-
76
64
  def within_validity_period?
77
65
  valid_until > Time.current && !expired?
78
66
  end
@@ -162,6 +150,23 @@ module Dscf
162
150
  order
163
151
  end
164
152
 
153
+ def build_from_rfq_items(items_attributes)
154
+ items_attributes.each do |item_attrs|
155
+ rfq_item = request_for_quotation.rfq_items.find_by(id: item_attrs[:rfq_item_id])
156
+ next unless rfq_item
157
+
158
+ quotation_items.build(
159
+ rfq_item: rfq_item,
160
+ product: rfq_item.product,
161
+ unit: item_attrs[:unit_id] ? Dscf::Marketplace::Unit.find(item_attrs[:unit_id]) : rfq_item.unit,
162
+ quantity: [ item_attrs[:quantity].to_i, rfq_item.quantity ].min,
163
+ unit_price: item_attrs[:unit_price].to_f,
164
+ notes: item_attrs[:notes] || ""
165
+ )
166
+ end
167
+ valid? # To trigger validations
168
+ end
169
+
165
170
  private
166
171
 
167
172
  def calculate_total_price
@@ -1,7 +1,7 @@
1
1
  module Dscf
2
2
  module Marketplace
3
3
  class QuotationItem < ApplicationRecord
4
- belongs_to :quotation, class_name: "Dscf::Marketplace::Quotation"
4
+ belongs_to :quotation, class_name: "Dscf::Marketplace::Quotation", inverse_of: :quotation_items
5
5
  belongs_to :rfq_item, class_name: "Dscf::Marketplace::RfqItem"
6
6
  belongs_to :product, class_name: "Dscf::Marketplace::Product"
7
7
  belongs_to :unit, class_name: "Dscf::Marketplace::Unit"
@@ -25,7 +25,7 @@ module Dscf
25
25
 
26
26
  # Ransack configuration for secure filtering
27
27
  def self.ransackable_attributes(_auth_object = nil)
28
- %w[id quotation_id rfq_item_id product_id unit_id quantity unit_price subtotal created_at updated_at]
28
+ %w[id quotation_id rfq_item_id product_id unit_id quantity unit_price subtotal notes created_at updated_at]
29
29
  end
30
30
 
31
31
  def self.ransackable_associations(_auth_object = nil)
@@ -4,7 +4,7 @@ module Dscf
4
4
  belongs_to :user, class_name: "Dscf::Core::User", optional: true if defined?(Dscf::Core)
5
5
  belongs_to :selected_quotation, class_name: "Dscf::Marketplace::Quotation", optional: true
6
6
 
7
- has_many :rfq_items, class_name: "Dscf::Marketplace::RfqItem", dependent: :destroy
7
+ has_many :rfq_items, class_name: "Dscf::Marketplace::RfqItem", dependent: :destroy, inverse_of: :request_for_quotation
8
8
  has_many :quotations, class_name: "Dscf::Marketplace::Quotation", dependent: :destroy
9
9
  accepts_nested_attributes_for :rfq_items, allow_destroy: true
10
10
 
@@ -1,7 +1,7 @@
1
1
  module Dscf
2
2
  module Marketplace
3
3
  class RfqItem < ApplicationRecord
4
- belongs_to :request_for_quotation, class_name: "Dscf::Marketplace::RequestForQuotation"
4
+ belongs_to :request_for_quotation, class_name: "Dscf::Marketplace::RequestForQuotation", inverse_of: :rfq_items
5
5
  belongs_to :product, class_name: "Dscf::Marketplace::Product"
6
6
  belongs_to :unit, class_name: "Dscf::Marketplace::Unit"
7
7
 
@@ -0,0 +1,55 @@
1
+ module Dscf
2
+ module Marketplace
3
+ class RfqResponseService
4
+ def initialize(current_user)
5
+ @current_user = current_user
6
+ end
7
+
8
+ def respond_to_rfq(rfq, params = {})
9
+ return {success: false, errors: [ "RFQ not found or invalid" ]} unless rfq.present? && rfq.sent?
10
+
11
+ business = Dscf::Core::Business.find_by(user: @current_user)
12
+ return {success: false, errors: [ "No business associated with user" ]} unless business.present?
13
+
14
+ quotation = Dscf::Marketplace::Quotation.new(
15
+ request_for_quotation: rfq,
16
+ business: business,
17
+ status: :draft,
18
+ notes: params[:notes] || "",
19
+ valid_until: params[:valid_until] || 7.days.from_now,
20
+ delivery_date: params[:delivery_date] || (params[:valid_until] || 7.days.from_now).to_date
21
+ )
22
+
23
+ if quotation.save
24
+ create_quotation_items(quotation, params[:quotation_items_attributes] || [])
25
+ rfq.update(status: :responded) if rfq.sent?
26
+ {success: true, quotation: quotation}
27
+ else
28
+ {success: false, errors: quotation.errors.full_messages}
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def create_quotation_items(quotation, items_attributes)
35
+ items_attributes.each do |item_attrs|
36
+ rfq_item = quotation.request_for_quotation.rfq_items.find_by(id: item_attrs[:rfq_item_id])
37
+ next unless rfq_item
38
+
39
+ unit_id = item_attrs[:unit_id] || rfq_item.unit_id
40
+ unit = Dscf::Marketplace::Unit.find_by(id: unit_id)
41
+
42
+ quotation.quotation_items.build(
43
+ rfq_item: rfq_item,
44
+ product: rfq_item.product,
45
+ unit: unit,
46
+ quantity: item_attrs[:quantity] || rfq_item.quantity,
47
+ unit_price: item_attrs[:unit_price],
48
+ notes: item_attrs[:notes] || ""
49
+ )
50
+ end
51
+ quotation.save
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,6 +1,7 @@
1
- # DSCF Marketplace Locale File
2
- # This file contains I18n messages for automatic message resolution
3
- # Pattern: {model_name}.{success|errors}.{action_name}
1
+ # Updated DSCF Marketplace Locale File
2
+ # Added missing keys for request_for_quotations (plural) to match controller usage
3
+ # Ensures full error messages are displayed instead of "Translation missing"
4
+ # Pattern: {model_name}.{success|errors}.{action_name} - using plural for controller consistency
4
5
 
5
6
  en:
6
7
  # Core Models
@@ -106,6 +107,28 @@ en:
106
107
  update: "Failed to update listing"
107
108
  destroy: "Failed to delete listing"
108
109
 
110
+ # RFQ Models (Added plural keys to match controller)
111
+ request_for_quotations:
112
+ success:
113
+ index: "RFQs retrieved successfully"
114
+ show: "RFQ details retrieved successfully"
115
+ create: "RFQ created successfully"
116
+ update: "RFQ updated successfully"
117
+ destroy: "RFQ deleted successfully"
118
+ sent: "RFQ sent successfully"
119
+ closed: "RFQ closed successfully"
120
+ responded: "RFQ response sent successfully"
121
+ errors:
122
+ index: "Failed to retrieve RFQs"
123
+ show: "Failed to retrieve RFQ details"
124
+ create_failed: "Failed to create RFQ" # Key fix: Matches render_error("request_for_quotations.errors.create_failed")
125
+ update: "Failed to update RFQ"
126
+ destroy: "Failed to delete RFQ"
127
+ send_failed: "Failed to send RFQ"
128
+ close_failed: "Failed to close RFQ"
129
+ respond_failed: "Failed to respond to RFQ"
130
+
131
+ # Singular fallback for consistency
109
132
  request_for_quotation:
110
133
  success:
111
134
  index: "RFQs retrieved successfully"
@@ -115,6 +138,7 @@ en:
115
138
  destroy: "RFQ deleted successfully"
116
139
  sent: "RFQ sent successfully"
117
140
  closed: "RFQ closed successfully"
141
+ responded: "RFQ response sent successfully"
118
142
  errors:
119
143
  index: "Failed to retrieve RFQs"
120
144
  show: "Failed to retrieve RFQ details"
@@ -123,6 +147,7 @@ en:
123
147
  destroy: "Failed to delete RFQ"
124
148
  sent: "Failed to send RFQ"
125
149
  closed: "Failed to close RFQ"
150
+ responded: "Failed to respond to RFQ"
126
151
 
127
152
  rfq_item:
128
153
  success:
@@ -351,3 +376,34 @@ en:
351
376
  operation_failed: "Operation failed"
352
377
  record_not_found: "Record not found"
353
378
  supplier_not_found: "Supplier not found"
379
+ # Added for common validation errors to show full messages
380
+ activerecord:
381
+ errors:
382
+ messages:
383
+ record_invalid: "Validation failed: %{errors}"
384
+ inclusion: "is not included in the list"
385
+ confirmation: "doesn't match %{attribute}"
386
+ accepted: "must be accepted"
387
+ empty: "can't be empty"
388
+ blank: "can't be blank"
389
+ too_short: "is too short (minimum is %{count} characters)"
390
+ too_long: "is too long (maximum is %{count} characters)"
391
+ invalid: "is invalid"
392
+ not_a_number: "is not a number"
393
+ not_an_integer: "is not an integer"
394
+ greater_than: "must be greater than %{count}"
395
+ greater_than_or_equal_to: "must be greater than or equal to %{count}"
396
+ equal_to: "must be equal to %{count}"
397
+ less_than: "must be less than %{count}"
398
+ less_than_or_equal_to: "must be less than or equal to %{count}"
399
+ other_than: "must be other than %{count}"
400
+ excluded: "should not be %{object}"
401
+ taken: "has already been taken"
402
+ unique: "has already been taken"
403
+ uniqueness: "has already been taken"
404
+ present: "must be present"
405
+ absence: "must be blank"
406
+ url: "is not a valid URL"
407
+ email: "is not a valid email"
408
+ format: "is invalid"
409
+ required: "can't be blank"
data/config/routes.rb CHANGED
@@ -50,6 +50,7 @@ Dscf::Marketplace::Engine.routes.draw do
50
50
  member do
51
51
  post "send_rfq"
52
52
  post "close"
53
+ post "respond"
53
54
  end
54
55
  collection do
55
56
  get "my_rfqs"
@@ -0,0 +1,5 @@
1
+ class AddNotesToDscfMarketplaceQuotationItems < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :dscf_marketplace_quotation_items, :notes, :text
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  module Dscf
2
2
  module Marketplace
3
- VERSION = "0.2.92".freeze
3
+ VERSION = "0.2.94".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dscf-marketplace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.92
4
+ version: 0.2.94
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-09 00:00:00.000000000 Z
10
+ date: 2025-09-12 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -416,7 +416,6 @@ extra_rdoc_files: []
416
416
  files:
417
417
  - MIT-LICENSE
418
418
  - Rakefile
419
- - app/controllers/concerns/dscf/marketplace/nested_creatable.rb
420
419
  - app/controllers/dscf/marketplace/application_controller.rb
421
420
  - app/controllers/dscf/marketplace/businesses_controller.rb
422
421
  - app/controllers/dscf/marketplace/categories_controller.rb
@@ -470,6 +469,7 @@ files:
470
469
  - app/serializers/dscf/marketplace/unit_conversion_serializer.rb
471
470
  - app/serializers/dscf/marketplace/unit_serializer.rb
472
471
  - app/services/dscf/marketplace/my_resource_service.rb
472
+ - app/services/dscf/marketplace/rfq_response_service.rb
473
473
  - config/environments/production.rb
474
474
  - config/locales/en.yml
475
475
  - config/routes.rb
@@ -497,6 +497,7 @@ files:
497
497
  - db/migrate/20250901080134_add_fulfillment_type_to_orders.rb
498
498
  - db/migrate/20250903061154_add_ordered_fields_to_orders.rb
499
499
  - db/migrate/20250903061306_populate_ordered_fields_for_existing_orders.rb
500
+ - db/migrate/20250909092700_add_notes_to_dscf_marketplace_quotation_items.rb
500
501
  - lib/dscf/marketplace.rb
501
502
  - lib/dscf/marketplace/engine.rb
502
503
  - lib/dscf/marketplace/version.rb
@@ -1,47 +0,0 @@
1
- module Dscf::Marketplace::NestedCreatable
2
- extend ActiveSupport::Concern
3
-
4
- included do
5
- # Override the create method from Dscf::Core::Common
6
- def create
7
- obj = @clazz.new(model_params)
8
- assign_current_user(obj) if respond_to?(:assign_current_user)
9
-
10
- if obj.save
11
- obj = @clazz.includes(eager_loaded_associations).find(obj.id) if eager_loaded_associations.present?
12
-
13
- includes = serializer_includes_for_action(:create)
14
- options = {include: includes} if includes.present?
15
-
16
- # Get success message from locales
17
- model_name = @clazz.name.demodulize.underscore
18
- message_key = "#{model_name}.success.create"
19
- message = I18n.t(message_key, default: "#{model_name.titleize} created successfully")
20
-
21
- # Include associated items in response for orders
22
- if model_name == "order" && obj.respond_to?(:order_items)
23
- render_success(message, data: obj, order_items: obj.order_items, serializer_options: options, status: :created)
24
- else
25
- render_success(message, data: obj, serializer_options: options, status: :created)
26
- end
27
- else
28
- Rails.logger.error("#{model_name} save failed: #{obj.errors.full_messages}")
29
-
30
- # Get error message from locales
31
- error_key = "#{model_name}.errors.create"
32
- error_message = I18n.t(error_key, default: "Failed to create #{model_name}")
33
-
34
- render_error(error_message, status: :unprocessable_entity)
35
- end
36
- rescue => e
37
- render_error(e.message)
38
- end
39
- end
40
-
41
- private
42
-
43
- def assign_current_user(obj)
44
- obj.user = current_user if obj.respond_to?(:user=)
45
- obj.ordered_by = current_user if obj.respond_to?(:ordered_by=)
46
- end
47
- end