diff --git a/CHECKLIST.md b/CHECKLIST.md new file mode 100644 index 0000000..803c367 --- /dev/null +++ b/CHECKLIST.md @@ -0,0 +1,104 @@ +# Unit Test Checklist + +## 1. **General Structure** +### 1.1 Test Case Pattern +- **Test Case Format**: + - **Test Case Title**: Tên test case, mô tả ngắn gọn về mục tiêu của test. + - **Preconditions**: Các điều kiện cần thiết trước khi chạy test (data, trạng thái hệ thống,...). + - **Test Steps**: Các bước thực hiện test chi tiết. + - **Expected Result**: Kết quả mong đợi sau khi thực hiện test case. + +### 1.2 Categories of Test Cases +- **Positive Test Cases**: Kiểm tra các tình huống hệ thống hoạt động đúng với dữ liệu hợp lệ. +- **Negative Test Cases**: Kiểm tra các tình huống khi hệ thống gặp lỗi hoặc dữ liệu không hợp lệ. +- **Edge Cases**: Kiểm tra các trường hợp biên, dữ liệu cực trị hoặc các tình huống bất thường. +- **Exception Handling**: Kiểm tra các tình huống xảy ra lỗi, exception được xử lý đúng. +- **Performance**: Kiểm tra hiệu suất, tốc độ xử lý của hệ thống trong các tình huống khối lượng lớn. + +--- + +## 2. **Test Case Details** + +### ✅ **Test Case 1: process_orders(user_id)** +- **Preconditions**: + - Có ít nhất một đơn hàng hợp lệ trong hệ thống. +- **Test Steps**: + - Gọi `process_orders(user_id)`. +- **Expected Result**: + - Trả về `true` nếu tất cả các đơn hàng được xử lý thành công. + +--- + +### ✅ **Test Case 2: process_order(order, user_id)** +#### **2.1 Khi order type là 'A'** +- **Preconditions**: + - `order.type = 'A'` và đơn hàng có amount hợp lệ. +- **Test Steps**: + - Gọi `process_order(order, user_id)`. +- **Expected Result**: + - Nếu CSV tạo thành công, cập nhật `order.status = 'exported'`. + +#### **2.2 Khi order type là 'B'** +- **Preconditions**: + - `order.type = 'B'` và API trả về kết quả hợp lệ. +- **Test Steps**: + - Gọi `process_order(order, user_id)`. +- **Expected Result**: + - Cập nhật trạng thái của `order` dựa trên dữ liệu trả về từ API (processed, pending, error). + +#### **2.3 Khi order type không xác định** +- **Preconditions**: + - `order.type` không thuộc loại `A`, `B`, `C`. +- **Test Steps**: + - Gọi `process_order(order, user_id)`. +- **Expected Result**: + - Cập nhật trạng thái `order.status = 'unknown_type'`. + +--- + +### ✅ **Test Case 3: update_priority(order)** +- **Preconditions**: + - `order.amount` có giá trị khác nhau (lớn hơn và nhỏ hơn 200). +- **Test Steps**: + - Gọi `update_priority(order)`. +- **Expected Result**: + - Nếu `order.amount > 200`, cập nhật `order.priority = 'high'`. + - Nếu `order.amount <= 200`, cập nhật `order.priority = 'low'`. + +--- + +### ✅ **Test Case 4: save_order(order)** +- **Preconditions**: + - Đơn hàng có trạng thái hợp lệ và chưa lưu vào cơ sở dữ liệu. +- **Test Steps**: + - Gọi `save_order(order)`. +- **Expected Result**: + - Đơn hàng được lưu mà không có lỗi, trạng thái không thay đổi thành `'db_error'`. + +#### **4.1 Trường hợp Database exception** +- **Preconditions**: + - Gây ra exception trong quá trình lưu trữ (`DatabaseException`). +- **Test Steps**: + - Gọi `save_order(order)`. +- **Expected Result**: + - Đơn hàng sẽ có `status = 'db_error'`. + +--- + +### ✅ **Test Case 5: csv_generate(order, user_id)** +- **Preconditions**: + - `order.type = 'A'`, `csv_generate` được gọi với các tham số hợp lệ. +- **Test Steps**: + - Gọi `csv_generate(order, user_id)`. +- **Expected Result**: + - Tạo file CSV với các dữ liệu của `order`, bao gồm các trường như ID, Type, Amount, Flag, Status, Priority. + +#### **5.1 Trường hợp đơn hàng có giá trị cao** +- **Preconditions**: + - `order.amount > 150`. +- **Test Steps**: + - Gọi `csv_generate(order, user_id)`. +- **Expected Result**: + - CSV bao gồm dòng `['', '', '', '', 'Note', 'High value order']`. + +--- diff --git a/app/models/order.rb b/app/models/order.rb new file mode 100644 index 0000000..5876a09 --- /dev/null +++ b/app/models/order.rb @@ -0,0 +1,6 @@ +class Order < ApplicationRecord + enum status: { new_order: 0, exported: 1, export_failed: 2, processed: 3, pending: 4, error: 5, api_error: 6, api_failure: 7, completed: 8, in_progress: 9, unknown_type: 10, db_error: 11 } + enum priority: { low: 0, high: 1 } + + self.inheritance_column = :sti_type +end diff --git a/app/services/order_processing_service.rb b/app/services/order_processing_service.rb new file mode 100644 index 0000000..186ab09 --- /dev/null +++ b/app/services/order_processing_service.rb @@ -0,0 +1,90 @@ +class APIException < StandardError; end + +class DatabaseException < StandardError; end + +class OrderProcessingService + def initialize(api_client) + @api_client = api_client + end + + def process_orders(user_id) + orders = Order.where(user_id: user_id) + return false if orders.empty? + + orders.each do |order| + process_order(order, user_id) + update_priority(order) + save_order(order) + end + true + rescue StandardError + false + end + + private + + def process_order(order, user_id) + case order.type + when "A" + process_type_a(order, user_id) + when "B" + process_type_b(order) + when "C" + process_type_c(order) + else + order.update(status: :unknown_type) + end + end + + def process_type_a(order, user_id) + begin + csv_generate(order, user_id) + order.update(status: :exported) + rescue StandardError + order.update(status: :export_failed) + end + end + + def process_type_b(order) + begin + response = @api_client.call_api(order.id) + if response.status == "success" + if response.data >= 50 && order.amount < 100 + order.update(status: :processed) + elsif response.data < 50 || order.flag + order.update(status: :pending) + else + order.update(status: :error) + end + else + order.update(status: :api_error) + end + rescue APIException + order.update(status: :api_failure) + end + end + + def process_type_c(order) + order.update(status: order.flag ? :completed : :in_progress) + end + + def csv_generate(order, user_id) + csv_file_path = "orders_type_A_#{user_id}_#{Time.now.to_i}.csv" + + CSV.open(csv_file_path, "w") do |csv| + csv << ["ID", "Type", "Amount", "Flag", "Status", "Priority"] + csv << [order.id, order.type, order.amount, order.flag, order.status, order.priority] + csv << ["", "", "", "", "Note", "High value order"] if order.amount > 150 + end + end + + def update_priority(order) + order.update(priority: order.amount > 200 ? :high : :low) + end + + def save_order(order) + order.save! + rescue DatabaseException + order.update(status: :db_error) + end +end diff --git a/db/migrate/20250403135748_create_orders.rb b/db/migrate/20250403135748_create_orders.rb new file mode 100644 index 0000000..d59cf7d --- /dev/null +++ b/db/migrate/20250403135748_create_orders.rb @@ -0,0 +1,14 @@ +class CreateOrders < ActiveRecord::Migration[7.0] + def change + create_table :orders do |t| + t.references :user, foreign_key: true, null: false + t.string :type, null: false + t.integer :amount, null: false + t.boolean :flag, default: false + t.integer :status, default: 0, null: false + t.integer :priority, default: 0, null: false + + t.timestamps + end + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index b957de9..f4e01b5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_03_26_162746) do +ActiveRecord::Schema[7.1].define(version: 2025_04_03_135748) do + create_table "orders", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "type", null: false + t.integer "amount", null: false + t.boolean "flag", default: false + t.integer "status", default: 0, null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_orders_on_user_id" + end + create_table "users", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "email" @@ -20,4 +32,5 @@ t.datetime "updated_at", null: false end + add_foreign_key "orders", "users" end diff --git a/spec/factories/orders.rb b/spec/factories/orders.rb new file mode 100644 index 0000000..f8c9607 --- /dev/null +++ b/spec/factories/orders.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :order do + user_id { 123 } + type { 'A' } + amount { 100 } + flag { false } + status { :new_order } + priority { :low } + end +end diff --git a/spec/services/order_processing_service_spec.rb b/spec/services/order_processing_service_spec.rb new file mode 100644 index 0000000..ffa9601 --- /dev/null +++ b/spec/services/order_processing_service_spec.rb @@ -0,0 +1,283 @@ +require "rails_helper" +require "csv" + +RSpec.describe OrderProcessingService, type: :service do + let(:api_client) { instance_double("ApiClient") } + let(:service) { described_class.new(api_client) } + let(:user) { create(:user) } + let(:user_id) { user.id } + let(:order) { create(:order, type: "A", user_id: user.id) } + let(:order_b) { create(:order, type: "B", user_id: user.id) } + let(:order_c) { create(:order, type: "C", user_id: user.id) } + let(:orders) { [order, order_b, order_c] } + + describe "#process_orders" do + context "when there are orders" do + it "should return true when all orders are processed successfully" do + # Given + allow(Order).to receive(:where).with(user_id: user_id).and_return(orders) + allow(service).to receive(:process_order) + + # When + result = service.process_orders(user_id) + + # Then + expect(result).to be true + end + end + + context "when there are no orders" do + it "should return false when no orders are found" do + # Given + allow(Order).to receive(:where).with(user_id: user_id).and_return([]) + + # When + result = service.process_orders(user_id) + + # Then + expect(result).to be false + end + end + + context "when an error occurs during processing" do + it "should return false when an exception is raised" do + # Given + allow(Order).to receive(:where).with(user_id: user_id).and_return(orders) + allow(service).to receive(:process_order).and_raise(StandardError) + + # When + result = service.process_orders(user_id) + + # Then + expect(result).to be false + end + end + end + + describe "#process_order" do + context "when order type is A" do + context "when CSV generation succeeds" do + it "should mark the order as exported" do + # Given + allow(service).to receive(:csv_generate).with(order, user_id).and_return(true) + order.update(amount: 100) + + # When + service.send(:process_order, order, user_id) + + # Then + expect(order.status).to eq("exported") + end + end + + context "when CSV generation fails" do + it "should mark the order as export_failed" do + # Given + allow(service).to receive(:csv_generate).and_raise(StandardError) + + # When + service.send(:process_order, order, user_id) + + # Then + expect(order.status).to eq("export_failed") + end + end + end + + context "when order type is B" do + it "should process type B order successfully when API returns success and data >= 50 and order.amount < 100" do + # Given + response = double("response", status: "success", data: 60) + allow(api_client).to receive(:call_api).with(order_b.id).and_return(response) + order_b.update(amount: 90) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("processed") + end + + it "should process type B order as pending when API returns success and data < 50" do + # Given + response = double("response", status: "success", data: 40) + allow(api_client).to receive(:call_api).with(order_b.id).and_return(response) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("pending") + end + + it "should process type B order as pending when API returns success and order.flag is true" do + # Given + response = double("response", status: "success", data: 60) + order_b.flag = true + allow(api_client).to receive(:call_api).with(order_b.id).and_return(response) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("pending") + end + + it "should process type B order as error when API returns success and response.data >= 50 and order.amount >= 100" do + # Given + response = double("response", status: "success", data: 60) + order_b.update(amount: 150) + allow(api_client).to receive(:call_api).with(order_b.id).and_return(response) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("error") + end + + it "should set status to api_error if API call returns failure" do + # Given + response = double("response", status: "failure", data: 0) + allow(api_client).to receive(:call_api).with(order_b.id).and_return(response) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("api_error") + end + + it "should set status to api_failure if API call raises an exception" do + # Given + allow(api_client).to receive(:call_api).with(order_b.id).and_raise(APIException) + + # When + service.send(:process_order, order_b, user_id) + + # Then + expect(order_b.status).to eq("api_failure") + end + end + + context "when order type is C" do + it "should set status to completed if flag is true" do + # Given + order_c.flag = true + + # When + service.send(:process_order, order_c, user_id) + + # Then + expect(order_c.status).to eq("completed") + end + + it "should set status to in_progress if flag is false" do + # Given + order_c.flag = false + + # When + service.send(:process_order, order_c, user_id) + + # Then + expect(order_c.status).to eq("in_progress") + end + end + + context "when order type is unknown" do + it "should set status to unknown_type" do + # Given + unknown_order = create(:order, type: "D", user_id: user.id) + + # When + service.send(:process_order, unknown_order, user_id) + + # Then + expect(unknown_order.status).to eq("unknown_type") + end + end + end + + describe "#update_priority" do + it "should set priority to high if amount is greater than 200" do + # Given + order.update(amount: 250) + + # When + service.send(:update_priority, order) + + # Then + expect(order.priority).to eq("high") + end + + it "should set priority to low if amount is less than or equal to 200" do + # Given + order.update(amount: 150) + + # When + service.send(:update_priority, order) + + # Then + expect(order.priority).to eq("low") + end + end + + describe "#save_order" do + context "when order is saved successfully" do + it "should save the order without any errors" do + # Given + allow(order).to receive(:save!).and_return(true) + + # When + service.send(:save_order, order) + + # Then + expect(order.status).not_to eq("db_error") + end + end + + context "when database error occurs" do + it "should set status to db_error if DatabaseException is raised" do + # Given + allow(order).to receive(:save!).and_raise(DatabaseException) + + # When + service.send(:save_order, order) + + # Then + expect(order.status).to eq("db_error") + end + end + end + + describe "#csv_generate" do + let(:order) { create(:order, user_id: user_id, type: "A", amount: 100, flag: true, status: :pending, priority: :low) } + let(:high_value_order) { create(:order, user_id: user_id, type: "A", amount: 200, flag: false, status: :processed, priority: :high) } + let(:csv_filename) { "orders_type_A_#{user_id}_#{Time.now.to_i}.csv" } + let(:mock_csv) { [] } + + before do + allow(Time).to receive(:now).and_return(Time.at(1_701_234_567)) + allow(CSV).to receive(:open).and_yield(mock_csv) + end + + it "should generate a CSV file with correct data when given a valid order" do + # When + service.send(:csv_generate, order, user_id) + + # Then + expect(mock_csv).to eq([ + ["ID", "Type", "Amount", "Flag", "Status", "Priority"], + [order.id, order.type, order.amount, order.flag, order.status, order.priority] + ]) + end + + it "should include a high value order note when amount is greater than 150" do + # When + service.send(:csv_generate, high_value_order, user_id) + + # Then + expect(mock_csv).to include(["", "", "", "", "Note", "High value order"]) + end + end +end