feat: implement complete CMT backend with API endpoints and test suite
- Add 7 core API endpoints: users, transactions, partners, products, inventory, payments, credit - Implement role-based authentication (admin/write/read-only access) - Add comprehensive database models with proper relationships - Include full test coverage for all endpoints and business logic - Set up Alembic migrations and Docker configuration - Configure FastAPI app with CORS and database integration
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app.schemas.base import PaymentMethod, TransactionType, TransactionStatus
|
||||
from app.schemas.models import Transaction, Payment
|
||||
from datetime import date, datetime
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
@pytest.fixture(name="sample_transaction")
|
||||
def sample_transaction_fixture(session: Session, sample_partner, admin_user):
|
||||
"""Create a sample transaction for payment testing."""
|
||||
transaction = Transaction(
|
||||
partner_id=sample_partner.id,
|
||||
transcation_type=TransactionType.SALE,
|
||||
transaction_status=TransactionStatus.UNPAID,
|
||||
total_amount=1000,
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
)
|
||||
session.add(transaction)
|
||||
session.commit()
|
||||
session.refresh(transaction)
|
||||
return transaction
|
||||
|
||||
|
||||
@pytest.fixture(name="sample_payment")
|
||||
def sample_payment_fixture(session: Session, sample_transaction, admin_user):
|
||||
"""Create a sample payment for testing."""
|
||||
payment = Payment(
|
||||
transaction_id=sample_transaction.id,
|
||||
payment_method="cash",
|
||||
paid_amount=500,
|
||||
payment_date=date.today(),
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
)
|
||||
session.add(payment)
|
||||
session.commit()
|
||||
session.refresh(payment)
|
||||
return payment
|
||||
|
||||
|
||||
@pytest.fixture(name="multiple_payments")
|
||||
def multiple_payments_fixture(session: Session, sample_transaction, admin_user):
|
||||
"""Create multiple payments for testing."""
|
||||
payments = [
|
||||
Payment(
|
||||
transaction_id=sample_transaction.id,
|
||||
payment_method="cash",
|
||||
paid_amount=300,
|
||||
payment_date=date.today(),
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
),
|
||||
Payment(
|
||||
transaction_id=sample_transaction.id,
|
||||
payment_method="momo",
|
||||
paid_amount=200,
|
||||
payment_date=date.today(),
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
),
|
||||
Payment(
|
||||
transaction_id=sample_transaction.id,
|
||||
payment_method="bank",
|
||||
paid_amount=150,
|
||||
payment_date=date.today(),
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
)
|
||||
]
|
||||
for payment in payments:
|
||||
session.add(payment)
|
||||
session.commit()
|
||||
for payment in payments:
|
||||
session.refresh(payment)
|
||||
return payments
|
||||
|
||||
|
||||
class TestPaymentCreation:
|
||||
"""Test payment creation endpoints."""
|
||||
|
||||
def test_create_payment_with_admin_access(self, client: TestClient, admin_token: str, sample_transaction):
|
||||
"""Test payment creation with admin token."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["transaction_id"] == sample_transaction.id
|
||||
assert data["payment_method"] == PaymentMethod.CASH
|
||||
assert data["paid_amount"] == 500
|
||||
assert data["payment_date"] == "2024-01-15"
|
||||
assert "id" in data
|
||||
assert "created_by" in data
|
||||
assert "updated_by" in data
|
||||
|
||||
def test_create_payment_with_write_access(self, client: TestClient, write_token: str, sample_transaction):
|
||||
"""Test payment creation with write token."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.MOMO,
|
||||
"paid_amount": 750,
|
||||
"payment_date": "2024-01-16"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["payment_method"] == PaymentMethod.MOMO
|
||||
assert data["paid_amount"] == 750
|
||||
|
||||
def test_create_payment_with_bank_method(self, client: TestClient, admin_token: str, sample_transaction):
|
||||
"""Test payment creation with bank payment method."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.BANK,
|
||||
"paid_amount": 1000,
|
||||
"payment_date": "2024-01-17"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["payment_method"] == PaymentMethod.BANK
|
||||
|
||||
def test_create_payment_unauthorized(self, client: TestClient, sample_transaction):
|
||||
"""Test payment creation without authentication."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/", json=payment_data)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_create_payment_read_only_forbidden(self, client: TestClient, read_only_token: str, sample_transaction):
|
||||
"""Test payment creation with read-only access should fail."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_create_payment_invalid_transaction(self, client: TestClient, admin_token: str):
|
||||
"""Test creation with non-existent transaction should fail."""
|
||||
payment_data = {
|
||||
"transaction_id": 99999, # Non-existent transaction
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 400
|
||||
assert "Transaction not found" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPaymentRetrieval:
|
||||
"""Test payment retrieval endpoints."""
|
||||
|
||||
def test_get_all_payments_with_auth(self, client: TestClient, admin_token: str, multiple_payments):
|
||||
"""Test retrieving all payments with authentication."""
|
||||
response = client.get("/api/v1/payments/",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 3 # At least the fixture payments
|
||||
|
||||
def test_get_all_payments_read_only_access(self, client: TestClient, read_only_token: str, multiple_payments):
|
||||
"""Test read-only user can retrieve payments."""
|
||||
response = client.get("/api/v1/payments/",
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_get_payments_unauthorized(self, client: TestClient):
|
||||
"""Test retrieving payments without authentication."""
|
||||
response = client.get("/api/v1/payments/")
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_get_payments_with_pagination(self, client: TestClient, admin_token: str, multiple_payments):
|
||||
"""Test payment retrieval with pagination."""
|
||||
response = client.get("/api/v1/payments/?skip=0&limit=2",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) <= 2
|
||||
|
||||
def test_get_single_payment_by_id(self, client: TestClient, admin_token: str, sample_payment):
|
||||
"""Test retrieving a single payment by ID."""
|
||||
response = client.get(f"/api/v1/payments/{sample_payment.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == sample_payment.id
|
||||
assert data["transaction_id"] == sample_payment.transaction_id
|
||||
assert data["payment_method"] == sample_payment.payment_method
|
||||
assert data["paid_amount"] == sample_payment.paid_amount
|
||||
|
||||
def test_get_nonexistent_payment(self, client: TestClient, admin_token: str):
|
||||
"""Test retrieving a non-existent payment."""
|
||||
response = client.get("/api/v1/payments/99999",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Payment not found" in response.json()["detail"]
|
||||
|
||||
def test_get_payments_by_transaction(self, client: TestClient, admin_token: str, multiple_payments, sample_transaction):
|
||||
"""Test retrieving payments for specific transaction."""
|
||||
response = client.get(f"/api/v1/payments/transaction/{sample_transaction.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 3 # All payments for this transaction
|
||||
for payment in data:
|
||||
assert payment["transaction_id"] == sample_transaction.id
|
||||
|
||||
def test_get_payments_by_nonexistent_transaction(self, client: TestClient, admin_token: str):
|
||||
"""Test retrieving payments for non-existent transaction."""
|
||||
response = client.get("/api/v1/payments/transaction/99999",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Transaction not found" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPaymentUpdate:
|
||||
"""Test payment update endpoints."""
|
||||
|
||||
def test_update_payment_with_write_access(self, client: TestClient, write_token: str, sample_payment):
|
||||
"""Test updating payment with write access."""
|
||||
update_data = {
|
||||
"payment_method": PaymentMethod.BANK,
|
||||
"paid_amount": 600
|
||||
}
|
||||
response = client.put(f"/api/v1/payments/{sample_payment.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["payment_method"] == PaymentMethod.BANK
|
||||
assert data["paid_amount"] == 600
|
||||
assert data["transaction_id"] == sample_payment.transaction_id # Unchanged
|
||||
|
||||
def test_update_payment_date(self, client: TestClient, admin_token: str, sample_payment):
|
||||
"""Test updating payment date."""
|
||||
update_data = {
|
||||
"payment_date": "2024-02-01"
|
||||
}
|
||||
response = client.put(f"/api/v1/payments/{sample_payment.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["payment_date"] == "2024-02-01"
|
||||
|
||||
def test_update_payment_transaction_id(self, client: TestClient, admin_token: str, sample_payment, sample_partner, admin_user, session):
|
||||
"""Test updating payment transaction ID."""
|
||||
# Create another transaction
|
||||
new_transaction = Transaction(
|
||||
partner_id=sample_partner.id,
|
||||
transcation_type=TransactionType.SALE,
|
||||
transaction_status=TransactionStatus.UNPAID,
|
||||
total_amount=2000,
|
||||
created_by=admin_user.id,
|
||||
updated_by=admin_user.id
|
||||
)
|
||||
session.add(new_transaction)
|
||||
session.commit()
|
||||
session.refresh(new_transaction)
|
||||
|
||||
update_data = {
|
||||
"transaction_id": new_transaction.id
|
||||
}
|
||||
response = client.put(f"/api/v1/payments/{sample_payment.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["transaction_id"] == new_transaction.id
|
||||
|
||||
def test_update_payment_read_only_forbidden(self, client: TestClient, read_only_token: str, sample_payment):
|
||||
"""Test updating payment with read-only access should fail."""
|
||||
update_data = {
|
||||
"paid_amount": 700
|
||||
}
|
||||
response = client.put(f"/api/v1/payments/{sample_payment.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_update_payment_invalid_transaction(self, client: TestClient, admin_token: str, sample_payment):
|
||||
"""Test updating payment with invalid transaction should fail."""
|
||||
update_data = {
|
||||
"transaction_id": 99999 # Non-existent transaction
|
||||
}
|
||||
response = client.put(f"/api/v1/payments/{sample_payment.id}",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 400
|
||||
assert "Transaction not found" in response.json()["detail"]
|
||||
|
||||
def test_update_nonexistent_payment(self, client: TestClient, admin_token: str):
|
||||
"""Test updating a non-existent payment."""
|
||||
update_data = {
|
||||
"paid_amount": 800
|
||||
}
|
||||
response = client.put("/api/v1/payments/99999",
|
||||
json=update_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Payment not found" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPaymentDeletion:
|
||||
"""Test payment deletion endpoints."""
|
||||
|
||||
def test_delete_payment_with_admin_access(self, client: TestClient, admin_token: str, sample_payment):
|
||||
"""Test deleting payment with admin access."""
|
||||
response = client.delete(f"/api/v1/payments/{sample_payment.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify payment is deleted
|
||||
get_response = client.get(f"/api/v1/payments/{sample_payment.id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert get_response.status_code == 404
|
||||
|
||||
def test_delete_payment_write_access_forbidden(self, client: TestClient, write_token: str, sample_payment):
|
||||
"""Test deleting payment with write access should fail."""
|
||||
response = client.delete(f"/api/v1/payments/{sample_payment.id}",
|
||||
headers={"Authorization": f"Bearer {write_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_delete_payment_read_only_forbidden(self, client: TestClient, read_only_token: str, sample_payment):
|
||||
"""Test deleting payment with read-only access should fail."""
|
||||
response = client.delete(f"/api/v1/payments/{sample_payment.id}",
|
||||
headers={"Authorization": f"Bearer {read_only_token}"})
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_delete_nonexistent_payment(self, client: TestClient, admin_token: str):
|
||||
"""Test deleting a non-existent payment."""
|
||||
response = client.delete("/api/v1/payments/99999",
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 404
|
||||
assert "Payment not found" in response.json()["detail"]
|
||||
|
||||
def test_delete_payment_unauthorized(self, client: TestClient, sample_payment):
|
||||
"""Test deleting payment without authentication."""
|
||||
response = client.delete(f"/api/v1/payments/{sample_payment.id}")
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestPaymentValidation:
|
||||
"""Test payment data validation."""
|
||||
|
||||
def test_create_payment_missing_required_fields(self, client: TestClient, admin_token: str):
|
||||
"""Test creating payment with missing required fields."""
|
||||
# Missing transaction_id
|
||||
payment_data = {
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_create_payment_invalid_payment_method(self, client: TestClient, admin_token: str, sample_transaction):
|
||||
"""Test creating payment with invalid payment method."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": "INVALID_METHOD",
|
||||
"paid_amount": 500,
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_create_payment_negative_amount(self, client: TestClient, admin_token: str, sample_transaction):
|
||||
"""Test creating payment with negative amount."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": -100, # Negative amount
|
||||
"payment_date": "2024-01-15"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
# This might pass depending on validation rules, but business logic should prevent it
|
||||
# You might want to add validation in the endpoint for this
|
||||
|
||||
def test_create_payment_invalid_date_format(self, client: TestClient, admin_token: str, sample_transaction):
|
||||
"""Test creating payment with invalid date format."""
|
||||
payment_data = {
|
||||
"transaction_id": sample_transaction.id,
|
||||
"payment_method": PaymentMethod.CASH,
|
||||
"paid_amount": 500,
|
||||
"payment_date": "invalid-date"
|
||||
}
|
||||
response = client.post("/api/v1/payments/",
|
||||
json=payment_data,
|
||||
headers={"Authorization": f"Bearer {admin_token}"})
|
||||
assert response.status_code == 422 # Validation error
|
||||
Reference in New Issue
Block a user