{"id":31794438,"url":"https://github.com/kanutocd/cash_register_api","last_synced_at":"2025-10-10T19:45:42.365Z","repository":{"id":301784362,"uuid":"1009640165","full_name":"kanutocd/cash_register_api","owner":"kanutocd","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-11T03:14:01.000Z","size":178,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-11T07:44:59.736Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kanutocd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-27T13:12:55.000Z","updated_at":"2025-06-29T01:53:29.000Z","dependencies_parsed_at":"2025-07-05T09:32:20.558Z","dependency_job_id":null,"html_url":"https://github.com/kanutocd/cash_register_api","commit_stats":null,"previous_names":["kanutocd/cash_register_api"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kanutocd/cash_register_api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fcash_register_api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fcash_register_api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fcash_register_api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fcash_register_api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kanutocd","download_url":"https://codeload.github.com/kanutocd/cash_register_api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kanutocd%2Fcash_register_api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279005028,"owners_count":26083827,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-10T02:00:06.843Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-10T19:45:39.832Z","updated_at":"2025-10-10T19:45:42.352Z","avatar_url":"https://github.com/kanutocd.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cash Register API 🛒\n\nA Ruby on Rails 8 API backend for a cash register system with product management, cart functionality, and product promotion engine.\n\n[![Ruby Version](https://img.shields.io/badge/ruby-3.4.2-red.svg)](https://www.ruby-lang.org)\n[![Rails Version](https://img.shields.io/badge/rails-8.0.2-red.svg)](https://rubyonrails.org)\n[![Test Coverage](https://img.shields.io/badge/coverage-90%25+-brightgreen.svg)](rspec)\n[![Code Quality](https://img.shields.io/badge/rubocop-passing-brightgreen.svg)](rubocop)\n\n## 🚀 Features\n\n### ✨ Live Demo\n\nThe codebase was already deployed to an AWS EC2 t2.large instance with:\n\n- Ubuntu 24.04 LTS\n- PostgreSQL 16\n- Puma web server reverse-proxied by Nginx\n\n`curl` can be used to interact with the API endpoints like:\n\n```bash\ncurl -X GET http://54.169.235.10/api/v1/products\n```\n\nUnfortunately, only http/80 can be used since I don't have a domain name thus SSL cert is not available.\n\nOr you can use the frontend app to interact with the API. here's the link: https://d3vvbb4sx6mq4p.cloudfront.net\n\nThe SPA was built using React + TypeScript + Vite.\n\nThe frontend app is deployed to a CloudFront distribution with an S3 bucket as the origin.\nThe CF distribution together with a Lambda@Edge function was used as a workaround for the lack of SSL certificate in the backend API.\nSee the github repo for the frontend app: https://github.com/kanutocd/cash_register_spa\n\n### 🛍️ **Product Management**\n\n- **CRUD Operations**: Create, read, update, and soft-delete products\n- **Product Validation**: Comprehensive validation for code, names, prices, and descriptions\n- **Active Status**: Enable/disable products without data loss\n- **Rich Metadata**: Store detailed product information\n\n### 🎯 **Advanced Promotion System**\n\nSupport for three distinct promotion types:\n\n1. **Buy X Get Y Free**: `buy_x_get_y_free`\n\n   - Example: Buy 2 apples, get 1 free\n\n2. **Percentage Discount**: `percentage_discount`\n\n   - Example: 15% off when buying 3 or more items\n\n3. **Fixed Amount Discount**: `fixed_discount`\n   - Example: $2 off each item when buying 5 or more\n\n### 🛒 **Smart Cart Management**\n\n- **Real-time Calculations**: Automatic price and discount computation\n- **Quantity Management**: Add, update, remove items seamlessly\n- **Promotion Application**: Intelligent discount application based on quantity\n- **Cart Totals**: Detailed breakdown of subtotals, discounts, and finals\n\n### 🔒 **Enterprise-Ready**\n\n- **RESTful API**: Clean, predictable endpoints following REST conventions\n- **CORS Support**: Configured for cross-origin requests\n- **Error Handling**: Comprehensive error responses with detailed messages\n- **Test Coverage**: 90%+ test coverage with RSpec and FactoryBot\n\n## 📋 Prerequisites\n\n- **Ruby**: 3.4.2 or higher\n- **Rails**: 8.0.2 or higher\n- **PostgreSQL**: THE Database for production and development\n- **Bundler**: For dependency management\n\n## 🛠️ Installation\n\n### 1. Clone the Repository\n\n```bash\ngit clone https://github.com/kanutocd/cash_register_api.git\ncd cash_register_api\n```\n\n### 2. Install Dependencies\n\n```bash\nbundle install\n```\n\n### 3. Database Setup\n\n```bash\n# Create and migrate database\nrails db:create\nrails db:migrate\n\n# Seed with sample data\nrails db:seed\n```\n\n### 4. Start the Server\n\n```bash\nrails server\n```\n\nThe API will be available at `http://localhost:3000`\n\n## 🧪 Testing\n\n### Run All Tests\n\n```bash\n# Run the full test suite\nbundle exec rspec\n\n# Run with coverage report\nbundle exec rspec --format documentation\n```\n\n### Test Coverage\n\n```bash\n# Generate coverage report\nCOVERAGE=true bundle exec rspec\nopen coverage/index.html\n```\n\n### Code Quality\n\n```bash\n# Run RuboCop for code style\nbundle exec rubocop\n\n# Auto-fix violations\nbundle exec rubocop -A\n```\n\n## 📖 API Documentation\n\n### Base URL\n\n```\nhttp://localhost:3000/api/v1\n```\n\n### 🛍️ Products Endpoints\n\n#### Get All Products\n\n```http\nGET /products\n```\n\n**Response:**\n\n```json\n[\n  {\n    \"id\": 1,\n    \"name\": \"Apple\",\n    \"description\": \"Fresh red apples\",\n    \"price\": 1.5,\n    \"active\": true,\n    \"promotions\": [\n      {\n        \"id\": 1,\n        \"name\": \"Buy 2 Get 1 Free\",\n        \"promotion_type\": \"buy_x_get_y_free\",\n        \"trigger_quantity\": 2,\n        \"free_quantity\": 1,\n        \"active\": true\n      }\n    ]\n  }\n]\n```\n\n#### Create Product\n\n```http\nPOST /products\nContent-Type: application/json\n\n{\n  \"product\": {\n    \"name\": \"Orange\",\n    \"description\": \"Fresh citrus oranges\",\n    \"price\": 2.00,\n    \"active\": true\n  }\n}\n```\n\n#### Update Product\n\n```http\nPUT /products/:id\nContent-Type: application/json\n\n{\n  \"product\": {\n    \"name\": \"Updated Product Name\",\n    \"price\": 2.50\n  }\n}\n```\n\n#### Delete Product (Soft Delete)\n\n```http\nDELETE /products/:id\n```\n\n### 🎯 Promotions Endpoints\n\n#### Create Promotion\n\n```http\nPOST /products/:product_id/promotions\nContent-Type: application/json\n\n{\n  \"promotion\": {\n    \"name\": \"Summer Sale\",\n    \"promotion_type\": \"percentage_discount\",\n    \"trigger_quantity\": 3,\n    \"discount_percentage\": 20.0,\n    \"active\": true\n  }\n}\n```\n\n#### Update Promotion\n\n```http\nPUT /products/:product_id/promotions/:id\n```\n\n#### Delete Promotion\n\n```http\nDELETE /products/:product_id/promotions/:id\n```\n\n### 🛒 Cart Endpoints\n\n#### Get Cart Items\n\n```http\nGET /cart_items\n```\n\n**Response:**\n\n```json\n[\n  {\n    \"id\": 1,\n    \"quantity\": 2,\n    \"total_price\": 3.0,\n    \"discount_amount\": 0,\n    \"product\": {\n      \"id\": 1,\n      \"name\": \"Apple\",\n      \"price\": 1.5\n    }\n  }\n]\n```\n\n#### Add to Cart\n\n```http\nPOST /cart_items\nContent-Type: application/json\n\n{\n  \"cart_item\": {\n    \"product_id\": 1,\n    \"quantity\": 2\n  }\n}\n```\n\n#### Update Cart Item\n\n```http\nPUT /cart_items/:id\nContent-Type: application/json\n\n{\n  \"cart_item\": {\n    \"quantity\": 3\n  }\n}\n```\n\n#### Remove from Cart\n\n```http\nDELETE /cart_items/:id\n```\n\n#### Clear Cart\n\n```http\nDELETE /cart\n```\n\n#### Get Cart Total\n\n```http\nGET /cart/total\n```\n\n**Response:**\n\n```json\n{\n  \"subtotal\": 25.5,\n  \"total_discount\": 3.75,\n  \"total\": 21.75,\n  \"items_count\": 8\n}\n```\n\n## 🏗️ Architecture\n\n### Models\n\n#### Product Model\n\n```ruby\nclass Product \u003c ApplicationRecord\n  has_many :promotions, dependent: :destroy\n  has_many :cart_items, dependent: :destroy\n\n  validates :name, presence: true, uniqueness: true\n  validates :price, presence: true, numericality: { greater_than: 0 }\n  validates :description, presence: true\n\n  scope :active, -\u003e { where(active: true) }\nend\n```\n\n#### Promotion Model\n\n```ruby\nclass Promotion \u003c ApplicationRecord\n  belongs_to :product\n\n  validates :promotion_type, inclusion: {\n    in: %w[buy_x_get_y_free percentage_discount fixed_discount]\n  }\n\n  def apply_discount(quantity, unit_price)\n    # Complex discount calculation logic\n  end\nend\n```\n\n#### Cart Item Model\n\n```ruby\nclass CartItem \u003c ApplicationRecord\n  belongs_to :product\n\n  validates :quantity, presence: true, numericality: { greater_than: 0 }\n  validates :product_id, uniqueness: true\n\n  def total_price\n    # Price calculation with promotion logic\n  end\nend\n```\n\n### Controllers\n\nControllers follow RESTful conventions with proper error handling:\n\n```ruby\nclass Api::V1::ProductsController \u003c ApplicationController\n  before_action :set_product, only: [:show, :update, :destroy]\n\n  def index\n    @products = Product.active.includes(:promotions)\n    render json: @products.as_json(include: :promotions)\n  end\n\n  private\n\n  def product_params\n    params.require(:product).permit(:name, :description, :price, :active)\n  end\nend\n```\n\n## 🧪 Testing Strategy\n\n### RSpec Configuration\n\n- **Model Tests**: Comprehensive validation and association testing\n- **Controller Tests**: API endpoint testing with various scenarios\n- **Factory Bot**: Clean test data generation\n- **Database Cleaner**: Isolated test environment\n\n### Sample Test\n\n```ruby\nRSpec.describe Product, type: :model do\n  describe 'validations' do\n    it { should validate_presence_of(:name) }\n    it { should validate_uniqueness_of(:name) }\n    it { should validate_numericality_of(:price).is_greater_than(0) }\n  end\n\n  describe '#current_promotion' do\n    let(:product) { create(:product) }\n    let!(:active_promotion) { create(:promotion, product: product, active: true) }\n\n    it 'returns the first active promotion' do\n      expect(product.current_promotion).to eq(active_promotion)\n    end\n  end\nend\n```\n\n## 🚀 Deployment\n\n### Environment Variables\n\n```bash\n# .env (create from .env.example)\nRAILS_ENV=production\nDATABASE_URL=postgresql://user:pass@localhost/myapp_production\nSECRET_KEY_BASE=your_secret_key\nCORS_ORIGINS=https://yourfrontend.com\n```\n\n### Production Setup\n\n```bash\n# Precompile assets (if any)\nRAILS_ENV=production rails assets:precompile\n\n# Run migrations\nRAILS_ENV=production rails db:migrate\n\n# Start server\nRAILS_ENV=production rails server -p 3000\n```\n\n## 🔧 Configuration\n\n### CORS Configuration\n\n```ruby\n# config/initializers/cors.rb\nRails.application.config.middleware.insert_before 0, Rack::Cors do\n  allow do\n    origins '*' # Configure for production\n    resource '*',\n      headers: :any,\n      methods: [:get, :post, :put, :patch, :delete, :options, :head]\n  end\nend\n```\n\n## 📊 Performance Considerations\n\n### Database Optimization\n\n- **Indexes**: Proper indexing on frequently queried columns\n- **Includes**: Prevent N+1 queries with eager loading\n- **Scopes**: Efficient query scopes for common filters\n\n### Input Validation\n\n- **Strong Parameters**: Whitelist permitted parameters\n- **Model Validations**: Comprehensive validation rules\n- **SQL Injection Prevention**: Parameterized queries only\n\n### API Security\n\n```ruby\n# Rate limiting (add gem 'rack-attack')\nclass Rack::Attack\n  throttle('api/ip', limit: 300, period: 5.minutes) do |req|\n    req.ip if req.path.start_with?('/api/')\n  end\nend\n```\n\n---\n\n**Built with ❤️ using Ruby on Rails 8.0.2**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fcash_register_api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanutocd%2Fcash_register_api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanutocd%2Fcash_register_api/lists"}