Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/contracts/queries/order_forms_query_filters_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Queries
class OrderFormsQueryFiltersContract < Dry::Validation::Contract
params do
optional(:status).maybe do
value(:string, included_in?: OrderForm::STATUSES.keys.map(&:to_s)) |
array(:string, included_in?: OrderForm::STATUSES.keys.map(&:to_s))
end
optional(:customer_id).maybe { value(:string, format?: Regex::UUID) | array(:string, format?: Regex::UUID) }
optional(:number).maybe { value(:string) | array(:string) }
optional(:quote_number).maybe { value(:string) | array(:string) }
optional(:owner_id).maybe { value(:string, format?: Regex::UUID) | array(:string, format?: Regex::UUID) }
optional(:created_at_from).maybe(:time)
optional(:created_at_to).maybe(:time)
optional(:expires_at_from).maybe(:time)
optional(:expires_at_to).maybe(:time)
end
end
end
71 changes: 71 additions & 0 deletions app/controllers/api/v1/order_forms_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

module Api
module V1
class OrderFormsController < Api::BaseController
before_action :ensure_feature_flag!

def index
result = OrderFormsQuery.call(
organization: current_organization,
pagination: {
page: params[:page],
limit: params[:per_page] || PER_PAGE
},
filters: index_filters,
search_term: params[:search_term]
)

if result.success?
render(
json: ::CollectionSerializer.new(
result.order_forms,
::V1::OrderFormSerializer,
collection_name: "order_forms",
meta: pagination_metadata(result.order_forms)
)
)
else
render_error_response(result)
end
end

def show
order_form = current_organization.order_forms.find_by(id: params[:id])
return not_found_error(resource: "order_form") unless order_form

render_order_form(order_form)
end

private

def ensure_feature_flag!
forbidden_error(code: "feature_not_available") unless current_organization.feature_flag_enabled?(:order_forms)
end

def index_filters
{
status: params[:status],
customer_id: params[:customer_id],
number: params[:number],
quote_number: params[:quote_number],
owner_id: params[:owner_id],
created_at_from: params[:created_at_from],
created_at_to: params[:created_at_to],
expires_at_from: params[:expires_at_from],
expires_at_to: params[:expires_at_to]
}
end

def render_order_form(order_form)
render(
json: ::V1::OrderFormSerializer.new(order_form, root_name: "order_form")
)
end

def resource_name
"order_form"
end
end
end
end
2 changes: 1 addition & 1 deletion app/models/api_key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class ApiKey < ApplicationRecord

RESOURCES = %w[
activity_log add_on analytic api_log billable_metric coupon applied_coupon credit_note customer_usage
customer event fee invoice organization payment payment_receipt payment_request payment_method plan subscription lifetime_usage
customer event fee invoice organization order_form payment payment_receipt payment_request payment_method plan subscription lifetime_usage
tax wallet wallet_transaction webhook_endpoint webhook_jwt_public_key invoice_custom_section
billing_entity alert feature security_log quote
].freeze
Expand Down
1 change: 1 addition & 0 deletions app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class Customer < ApplicationRecord
has_many :add_ons, through: :applied_add_ons
has_many :daily_usages
has_many :quotes
has_many :order_forms
has_many :wallets
has_many :wallet_transactions, through: :wallets
has_many :payment_provider_customers,
Expand Down
14 changes: 7 additions & 7 deletions app/models/metadata/item_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ def value_correctness
# Table name: item_metadata
# Database name: primary
#
# id :uuid not null, primary key
# owner_type :string not null
# value :jsonb not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :uuid not null
# owner_id :uuid not null
# id :uuid not null, primary key
# owner_type(Polymorphic owner type) :string not null
# value(item_metadata key-value pairs) :jsonb not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id(Reference to the organization) :uuid not null
# owner_id(Polymorphic owner id) :uuid not null
#
# Indexes
#
Expand Down
91 changes: 91 additions & 0 deletions app/models/order_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

class OrderForm < ApplicationRecord
include Sequenced

STATUSES = {
generated: "generated",
signed: "signed",
expired: "expired",
voided: "voided"
}.freeze

VOID_REASONS = {
manual: "manual",
expired: "expired",
invalid: "invalid"
}.freeze

before_save :ensure_number

belongs_to :organization
belongs_to :customer
belongs_to :quote_version
belongs_to :signed_by_user, class_name: "User", optional: true
has_one :quote, through: :quote_version

enum :status, STATUSES,
default: :generated,
validate: true
enum :void_reason, VOID_REASONS,
instance_methods: false,
validate: {allow_nil: true}

validates :billing_snapshot, presence: true

sequenced(
scope: ->(order_form) { order_form.organization.order_forms },
lock_key: ->(order_form) { order_form.organization_id }
)

def self.ransackable_attributes(_ = nil)
%w[number]
end

private

def ensure_number
return if number.present?
return if sequential_id.blank?

time = created_at || Time.current
formatted_sequential_id = format("%04d", sequential_id)
self.number = "OF-#{time.strftime("%Y")}-#{formatted_sequential_id}"
end
end

# == Schema Information
#
# Table name: order_forms
# Database name: primary
#
# id :uuid not null, primary key
# billing_snapshot :jsonb not null
# expires_at :datetime
# number :string not null
# signed_at :datetime
# status :enum default("generated"), not null
# void_reason :enum
# voided_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# customer_id :uuid not null
# organization_id :uuid not null
# quote_version_id :uuid not null
# sequential_id :integer not null
# signed_by_user_id :uuid
Comment thread
toommz marked this conversation as resolved.
#
# Indexes
#
# index_order_forms_on_customer_id (customer_id)
# index_order_forms_on_quote_version_id (quote_version_id) UNIQUE
# index_unique_order_forms_on_organization_number (organization_id,number) UNIQUE
# index_unique_order_forms_on_organization_sequential_id (organization_id,sequential_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (customer_id => customers.id)
# fk_rails_... (organization_id => organizations.id)
# fk_rails_... (quote_version_id => quote_versions.id)
# fk_rails_... (signed_by_user_id => users.id)
#
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class Organization < ApplicationRecord
has_many :roles
has_many :quotes
has_many :quote_versions
has_many :order_forms
has_many :activity_logs, class_name: "Clickhouse::ActivityLog"
has_many :features, class_name: "Entitlement::Feature"
has_many :privileges, class_name: "Entitlement::Privilege"
Expand Down
1 change: 1 addition & 0 deletions app/models/quote.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Quote < ApplicationRecord

has_many :versions, -> { order(sequential_id: :desc) }, class_name: "QuoteVersion"
has_one :current_version, -> { order(sequential_id: :desc) }, class_name: "QuoteVersion"
has_many :order_forms, through: :versions

enum :order_type, ORDER_TYPES,
instance_methods: false,
Expand Down
1 change: 1 addition & 0 deletions app/models/quote_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class QuoteVersion < ApplicationRecord

belongs_to :organization
belongs_to :quote
has_one :order_form

enum :status, STATUSES,
default: :draft,
Expand Down
110 changes: 110 additions & 0 deletions app/queries/order_forms_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# frozen_string_literal: true

class OrderFormsQuery < BaseQuery
Result = BaseResult[:order_forms]
Filters = BaseFilters[
:status,
:customer_id,
:number,
:quote_number,
:owner_id,
:created_at_from,
:created_at_to,
:expires_at_from,
:expires_at_to
]

def call
return result unless validate_filters.success?

order_forms = base_scope.result
order_forms = with_status(order_forms) if filters.status.present?
order_forms = with_customer_id(order_forms) if filters.customer_id.present?
order_forms = with_number(order_forms) if filters.number.present?
order_forms = with_quote_number(order_forms) if filters.quote_number.present?
order_forms = with_owner_id(order_forms) if filters.owner_id.present?
order_forms = with_created_at_range(order_forms) if created_at_range?
order_forms = with_expires_at_range(order_forms) if expires_at_range?
order_forms = paginate(order_forms)
order_forms = apply_consistent_ordering(order_forms)

result.order_forms = order_forms
result
end

private

def filters_contract
@filters_contract ||= Queries::OrderFormsQueryFiltersContract.new
end

def base_scope
organization.order_forms.includes(:customer, :quote_version).ransack(search_params)
end

def search_params
return if search_term.blank?

{number_cont: search_term}
end

def with_status(scope)
scope.where(status: filters.status)
end

def with_customer_id(scope)
scope.where(customer_id: filters.customer_id)
end

def with_number(scope)
scope.where(number: filters.number)
end

def with_quote_number(scope)
scope.joins(quote_version: :quote).where(quotes: {number: filters.quote_number})
end

def with_owner_id(scope)
scope.where(
quote_version_id: QuoteVersion.where(
quote_id: QuoteOwner.where(user_id: filters.owner_id).select(:quote_id)
).select(:id)
)
end

def with_created_at_range(scope)
scope = scope.where(created_at: created_at_from..) if filters.created_at_from
scope = scope.where(created_at: ..created_at_to) if filters.created_at_to
scope
end

def with_expires_at_range(scope)
scope = scope.where(expires_at: expires_at_from..) if filters.expires_at_from
scope = scope.where(expires_at: ..expires_at_to) if filters.expires_at_to
scope
end

def created_at_range?
filters.created_at_from.present? || filters.created_at_to.present?
end

def expires_at_range?
filters.expires_at_from.present? || filters.expires_at_to.present?
end

def created_at_from
@created_at_from ||= parse_datetime_filter(:created_at_from)
end

def created_at_to
@created_at_to ||= parse_datetime_filter(:created_at_to)
end

def expires_at_from
@expires_at_from ||= parse_datetime_filter(:expires_at_from)
end

def expires_at_to
@expires_at_to ||= parse_datetime_filter(:expires_at_to)
end
end
25 changes: 25 additions & 0 deletions app/serializers/v1/order_form_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module V1
class OrderFormSerializer < ModelSerializer
def serialize
{
lago_id: model.id,
number: model.number,
status: model.status,
void_reason: model.void_reason,
billing_snapshot: model.billing_snapshot,
expires_at: model.expires_at&.iso8601,
signed_at: model.signed_at&.iso8601,
voided_at: model.voided_at&.iso8601,
lago_signed_by_user_id: model.signed_by_user_id,
lago_organization_id: model.organization_id,
lago_customer_id: model.customer_id,
lago_quote_id: model.quote_version.quote_id,
lago_quote_version_id: model.quote_version_id,
created_at: model.created_at.iso8601,
updated_at: model.updated_at.iso8601
}
end
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
post :resend_email, on: :member
end
resources :payment_requests, only: %i[create index show]
resources :order_forms, only: %i[show index]
resources :payments, only: %i[create index show]
resources :plans, param: :code, code: /.*/ do
resources :charges, only: %i[index show create update destroy], param: :code, code: /.*/, controller: "plans/charges" do
Expand Down
Loading
Loading