Skip to content
Merged
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
28 changes: 26 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands

### Development Server
- `bin/dev` - Start the development server using foreman/overmind (runs Rails server + Vite)
- `bin/dev` - Start the development server using foreman/overmind (runs Rails server + Vite + Telegram polling)
- `bin/rails s` - Start Rails server only
- `bin/vite dev` - Start Vite development server only

**Important**: Do NOT run `bin/dev` or `bin/rails s` - assume the development server is already running. Only use these commands if explicitly asked to start the server.

### Testing
- `bin/rails test` - Run all tests
- `bin/rails test test/models/` - Run model tests
Expand All @@ -33,6 +35,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `bin/rails "invitations:cleanup"` - Clean up expired invitations
- `bin/rails "reminders:send_daily"` - Send daily payment reminders

### Telegram Bot
- `ruby lib/telegram_polling.rb` - Start Telegram bot polling (runs automatically with `bin/dev`)
- Bot responds to commands: `/start`, `/help`, `/status`, `/payments`, `/pay`, `/settings`

## Application Architecture

### Stack Overview
Expand All @@ -46,9 +52,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Project**: Main subscription entity with cost splitting logic (`app/models/project.rb:158`)
- **BillingCycle**: Represents billing periods with payment tracking (`app/models/billing_cycle.rb:68`)
- **Payment**: Individual payment records with evidence uploads
- **User**: Authentication and profile management with magic links
- **User**: Authentication and profile management with magic links and Telegram integration
- **ProjectMembership**: Join table for project access control
- **Invitation**: Email-based invitation system for project members
- **TelegramMessage**: Telegram notification tracking and delivery status

### Key Business Logic
- **Currency Support**: Multi-currency support via `CurrencySupport` concern
Expand All @@ -75,11 +82,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Development**: Letter Opener for email preview at `/letter_opener`
- **Templates**: ERB templates in `app/views/*_mailer/` with both HTML and text versions

### Telegram Bot Integration
- **Bot Framework**: telegram-bot-ruby gem for Telegram API integration
- **Account Linking**: Users can link Telegram accounts via verification tokens in profile settings
- **Notifications**: Payment reminders, billing cycle alerts, and payment confirmations via Telegram
- **Bot Commands**: Interactive commands for payment management and status checking
- **Services**: `TelegramBotService` for API interactions, `TelegramNotificationService` for message formatting
- **Jobs**: `TelegramNotificationJob` for asynchronous delivery
- **Models**: `TelegramMessage` for delivery tracking and status
- **Polling**: Continuous webhook processing via `lib/telegram_polling.rb`

### Background Jobs
- **Queue**: SolidQueue for job processing
- **Billing Jobs**: Automatic billing cycle generation and archiving
- **Reminder Jobs**: Automated payment reminder system
- **Email Jobs**: Asynchronous email delivery
- **Telegram Jobs**: Asynchronous Telegram notification delivery

### File Storage
- **Active Storage**: For payment evidence uploads
Expand All @@ -98,6 +116,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Parameter Filtering**: Sensitive data excluded from logs
- **Error Handling**: Comprehensive exception handling with notifications

### Configuration & Environment
- **Credentials**: Telegram bot token stored in Rails credentials (`telegram_bot_token`)
- **Routes**: Telegram-specific routes in `config/routes.rb` for profile integration
- **Initializers**: Telegram configuration in `config/initializers/telegram.rb`
- **Development**: Telegram polling runs automatically with `bin/dev` via Procfile.dev

### Development Tools
- **Letter Opener**: Email preview in development
- **Web Console**: Debugging in development
Expand Down
7 changes: 4 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ gem "puma", ">= 5.0"
gem "jbuilder"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
gem "bcrypt", "~> 3.1"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
Expand Down Expand Up @@ -60,8 +60,6 @@ gem "inertia_rails", "~> 3.9"

gem "vite_rails", "~> 3.0", ">= 3.0.19"

gem "bcrypt", "~> 3.1"

gem "js-routes", "~> 2.3"

gem "kaminari", "~> 1.2"
Expand All @@ -76,3 +74,6 @@ gem "dotenv-rails", groups: [ :development, :test ]

# Email delivery via Resend API
gem "resend"

# Telegram bot integration
gem "telegram-bot-ruby"
67 changes: 53 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,41 @@ GEM
railties (>= 6.1)
drb (2.2.3)
dry-cli (1.2.0)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-struct (1.8.0)
dry-core (~> 1.1)
dry-types (~> 1.8, >= 1.8.2)
ice_nine (~> 0.11)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
ed25519 (1.4.0)
erb (5.0.1)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
faraday (2.13.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
Expand All @@ -126,6 +156,7 @@ GEM
multi_xml (>= 0.5.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
Expand Down Expand Up @@ -189,15 +220,17 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
mini_magick (5.2.0)
benchmark
mini_magick (5.3.0)
logger
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
multi_xml (0.7.2)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-imap (0.5.9)
date
net-protocol
Expand Down Expand Up @@ -289,7 +322,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.0)
rdoc (6.14.1)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
regexp_parser (2.10.0)
Expand All @@ -298,7 +331,7 @@ GEM
resend (0.22.0)
httparty (>= 0.21.0)
rexml (3.4.1)
rubocop (1.77.0)
rubocop (1.78.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
Expand Down Expand Up @@ -346,22 +379,22 @@ GEM
activerecord (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
solid_queue (1.1.5)
solid_queue (1.2.0)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (~> 1.3.1)
sorbet-runtime (0.5.12204)
sqlite3 (2.7.0-aarch64-linux-gnu)
sqlite3 (2.7.0-aarch64-linux-musl)
sqlite3 (2.7.0-arm-linux-gnu)
sqlite3 (2.7.0-arm-linux-musl)
sqlite3 (2.7.0-arm64-darwin)
sqlite3 (2.7.0-x86_64-darwin)
sqlite3 (2.7.0-x86_64-linux-gnu)
sqlite3 (2.7.0-x86_64-linux-musl)
sorbet-runtime (0.5.12222)
sqlite3 (2.7.2-aarch64-linux-gnu)
sqlite3 (2.7.2-aarch64-linux-musl)
sqlite3 (2.7.2-arm-linux-gnu)
sqlite3 (2.7.2-arm-linux-musl)
sqlite3 (2.7.2-arm64-darwin)
sqlite3 (2.7.2-x86_64-darwin)
sqlite3 (2.7.2-x86_64-linux-gnu)
sqlite3 (2.7.2-x86_64-linux-musl)
sshkit (1.24.0)
base64
logger
Expand All @@ -370,6 +403,11 @@ GEM
net-ssh (>= 2.8.0)
ostruct
stringio (3.1.7)
telegram-bot-ruby (2.4.0)
dry-struct (~> 1.6)
faraday (~> 2.0)
faraday-multipart (~> 1.0)
zeitwerk (~> 2.6)
thor (1.3.2)
thruster (0.1.14)
thruster (0.1.14-aarch64-linux)
Expand Down Expand Up @@ -441,6 +479,7 @@ DEPENDENCIES
solid_errors
solid_queue
sqlite3 (>= 2.1)
telegram-bot-ruby
thruster
tzinfo-data
vite_rails (~> 3.0, >= 3.0.19)
Expand Down
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/rails server -p $PORT -e $RAILS_ENV
telegram: ruby lib/telegram_polling.rb
1 change: 1 addition & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

vite: bin/vite dev
web: bin/rails s
telegram: ruby lib/telegram_polling.rb
40 changes: 20 additions & 20 deletions app/controllers/invitations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,13 @@ def confirm

# Check if user already exists
if User.exists?(email_address: user_email)
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { email: ["An account with this email already exists. Please sign in instead."] }
},
errors: { email: [ "An account with this email already exists. Please sign in instead." ] }
},
status: :unprocessable_entity
return
end
Expand All @@ -240,13 +240,13 @@ def confirm
begin
# Double-check if user exists right before creation to handle race conditions
if User.exists?(email_address: user_email)
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { email: ["An account with this email already exists. Please sign in instead."] }
},
errors: { email: [ "An account with this email already exists. Please sign in instead." ] }
},
status: :unprocessable_entity
return
end
Expand All @@ -270,68 +270,68 @@ def confirm
else
Rails.logger.error "Failed to accept invitation for user: #{user.id}"
user.destroy # Clean up if invitation acceptance fails
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { message: "Unable to accept invitation. Please try again." }
},
},
status: :unprocessable_entity
end
else
# Return validation errors
Rails.logger.error "User validation failed: #{user.errors.full_messages}"
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: user.errors.as_json
},
},
status: :unprocessable_entity
end
rescue ActiveRecord::RecordNotUnique => e
Rails.logger.error "RecordNotUnique error: #{e.message}"
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { email: ["An account with this email already exists. Please contact the project owner."] }
},
errors: { email: [ "An account with this email already exists. Please contact the project owner." ] }
},
status: :unprocessable_entity
rescue ActiveRecord::StatementInvalid => e
Rails.logger.error "StatementInvalid error: #{e.message}"
if e.message.include?("UNIQUE constraint failed") || e.message.include?("duplicate key")
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { email: ["An account with this email already exists. Please contact the project owner."] }
},
errors: { email: [ "An account with this email already exists. Please contact the project owner." ] }
},
status: :unprocessable_entity
else
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { message: "Something went wrong while creating your account. Please try again or contact support." }
},
},
status: :unprocessable_entity
end
rescue StandardError => e
Rails.logger.error "Error in invitation confirmation: #{e.class.name} - #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render inertia: "invitations/confirm",
render inertia: "invitations/confirm",
props: {
invitation: invitation_props(@invitation),
project: project_with_details(@invitation.project),
user_email: @invitation.email,
errors: { message: "Something went wrong while creating your account. Please try again or contact support." }
},
},
status: :unprocessable_entity
end
end
Expand Down
Loading
Loading