| description | General Rails rules |
|---|---|
| globs | app/**/*.rb |
| alwaysApply | true |
This projects runs in docker container, managed with docker-compose.
You must run rspec in the api container, use lago exec api bundle exec rspec <args>.
You must use the rails cli in the container too, for example: lago exec api bin/rails db:migrate.
- Never use
OpenStruct - avoid
if/unlessmodifier right before the last line. USEAVOIDif something this else that endreturn than unless something this
All commit messages must follow the Conventional Commits specification:
<type>[optional scope]: <description>
## Context
...
## Description
...
Where:
<type>is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert, misc[optional scope]is optional and describes the area of change (e.g., auth, billing, api)<description>is a short description of the change in imperative mood
When generating or amending commit messages:
- The first line must be 50 characters or less
- Use the imperative mood ("Add feature" not "Added feature")
- The body should
- Explain the context and rationale for the change
- Explain the "why" and "what" at a conceptual level, not the "how" at a code level
- Be simple and direct using complete sentences without being verbose while keeping as much information as possible
- Check the whole diff at once using
PAGER=cat git diff ...to see all changes together - Generate the commit message based on the actual changes, not assumptions
- Do not check previous commits or commit history
- Only describe what was actually added or changed, not what already existed in the files
When creating new commits:
- Only analyze the current staged changes (
PAGER=cat git diff --staged)
When amending commits:
- Always check the actual commit content first using
PAGER=cat git show HEADto see all changes andPAGER=cat git diff --stagedto see staged changes - Use
git commit --amend -m "message"to update the commit message
- Creating, updating an deleting model must be done using a dedicated service, unless instructed otherwise.
For instance, to create an Alert model, you should create a
CreateAlertServiceclass. - Before deleting a model, inspect it to determine if it's soft deletable (it includes
Discard::Model). If soft deletable, usemodel.discard. Never hard delete a soft deletable model.
When creating a service class:
- the class always extend BaseService using
< - the class name should always end with
Service - the class should always be placed in
app/services/**/*.rb - Service class takes named arguments via the constructor and arguments are stored in instance variables.
- Each instance variable should have a private attr_reader declared
- Service class have one and only one public method named
calland it never accepts arguments - Service
callmethod should always returnresult - Service class must define a custom Result class following these rules:
- By default,
Result = BaseResult - If the service must return values, define them using
BaseResult[]. Example of result returning a customer and a subcription:Result = BaseResult[:customer, :subscription]
- By default,
To call the class service class asynchronously, create job:
- jobs should have the exact same fully qualified class name except it ends with
Jobinstead ofService. - the perform method of the job typically calls the matching service and forwards all it's arguements
- the service is called using the class method
call! - avoid using named parameters for jobs
Example of job calling a service:
# frozen_string_literal: true
module SomeModuleName
class MyGeneratedJob < ApplicationJob
queue_as "default"
def perform(organization, subscription)
SomeModuleName::MyGeneratedService.call!(organization:, subscription:)
end
end
end- Under
V1namespace the resource retrieved should always be scoped to the current_organization. Typically, to retrieve Alerts, usecurrent_organization.alerts.where(...) - In controller
createmethod, return regular 200 status, avoidstatus: :created - When testing controller, access the response via
jsonmethod, which parsed json and symbolized keys.
- New models must directly belong to an organization. Store the
organization_idin the table, don't usethrough:
Soft deletion
- not all models are soft deletable
- soft deletable models must
include Discard::Model - the soft deletion column is called
deleted_at - soft deletable models must use
default_scope -> { kept } - soft deletable models should be deletable
- You cannot rely on
dependent: :destroyif the model is soft deleted, you must calldiscard_all!on relationship manually
- Define enum constants as arrays before using them in enum declarations
- For PostgreSQL enums, define constants as hashes with string values, not arrays. Use the format:
ENUM_NAME = { value1: "value1", value2: "value2" }.freeze. - New model enums should always use
validate: true
Example:
FEE_TYPES = %i[charge add_on subscription credit commitment].freeze
enum :fee_type, FEE_TYPES
ON_TERMINATION_CREDIT_NOTES = { credit: "credit", omit: "omit" }.freeze
enum :on_termination_credit_note, ON_TERMINATION_CREDIT_NOTESTo create a webhook:
- A webhook name is typically
resource.action, for example:customer.updatedoralert.triggered. Use other webhooks as example to follow. - Create a service in
app/services/webhooks/, typically namedWebhooks::ResourceActionServicelikeWebhooks::CustomerUpdatedService - A service must define at least the following methods:
current_organization- how to get the organization from the modelobject_serializerwhich typically calls a serializer classwebhook_typealways the name likeresource.actionobject_typewhich is the object serialized. Reuse this method in the serializerroot_nameparam
- Add the mapping
name=> Service class to theSendWebhookJob::WEBHOOK_SERVICEShash - Write a test for the webhook class
- Make sure to specify the latest available
ActiveRecord::Migrationversion. For example, if the latest version is8.0, useActiveRecord::Migration[8.0]. - Prefer
add_columnoverchange_tablewhen adding single columns - Use
safety_assuredwrapper when required for complex operations - Never use hardcoded or fake timestamps in migration filenames. Migration timestamps should be generated using
date +"%Y%m%d%H%M%S"command to ensure proper chronological ordering. - For enums, use
create_enumto define the PostgreSQL enum type before adding the column- Enum type names should be descriptive and include the table/model context (e.g.,
subscription_on_termination_credit_note)
- Enum type names should be descriptive and include the table/model context (e.g.,
- New optional parameters must not break existing functionality
- Use descriptive error messages that explain why validation failed
- Always validate enum values in a service validation class to prevent invalid API input
- When using ransack with search_params, make sure the attributes are defined in the model class method
self.ransackable_attributes(_auth_object = nil)
- Do not test
#initializemethod. - In controller specs, use
get_with_tokenand similar method, don't try to mock the token manually - to test a "resource not found error" from an
Api::V1controller, use the custom matchbe_not_found_errorlike this:expect(response).to be_not_found_error("alert") - Prefer
expect(...).to have_received()instead ofexpect(...).to receive() - never use
aggregate_failurein new test. Do not edit existing tests to remove it. - After making changes to the tests, always run the tests to ensure they pass.
- When doing array comparison in tests, use
eqormatch_arrayinstead of multipleinclude/not_to includeassertions when the expected array is small enough to be readable - Use single-line
letstatements when they fit on one line without breaking Rubocop rules - Use
let!only for objects that need to be created before the test runs; if not referencing the object in tests, consider creating them directly in abeforeblock instead of usinglet - Run as minimum number of tests as possible. Narrow down run tests for specific describe or file.
- When testing models, test all enums and group them all in a
describe "enums"block with a singleitblock (not multipleitblocks)- When testing PostgreSQL enums, use the
.backed_by_column_of_type(:enum)matcher:expect(subject).to define_enum_for(:on_termination_credit_note) .backed_by_column_of_type(:enum) .validating .with_values(credit: "credit", omit: "omit")
- When testing PostgreSQL enums, use the
- When testing models, test ALL associations (belongs_to, has_one, has_many, etc.) and group them all in a
describe "associations"block with a singleitblock (not multipleitblocks)- Include ALL association parameters and options (class_name, foreign_key, through, dependent, autosave, optional, etc.)
- Clickhouse associations should have their own
describe "Clickhouse associations"block withclickhouse: truemetadata after the associations block
- When testing models, test all scopes and group them all in a
describe "Scopes"block with individualdescribe ".scope_name"blocks for each scope - When testing models, test all validations and group them all in a
describe "validations"block with a singleitblock- For complex custom validations, use a nested
describe "attribute_name validation"block instead of using the method name
- For complex custom validations, use a nested
- Test sections should appear in this order: enums, associations, Clickhouse associations, scopes, validations
- Some factories have been renamed for clarity.
- To create Entitlement::Feature model, use
:feature - To create Entitlement::Privilege model, use
:privilege - To create Entitlement::Entitlement model, use
:entitlement
- To create Entitlement::Feature model, use