Skip to content
Closed
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
59 changes: 59 additions & 0 deletions .github/workflows/github-actions-demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: NETEASE-CICD

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
test:
name: ✅ Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: 🔍 Checkout code
uses: actions/checkout@v3

- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install uv
run: |
curl -Ls https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH

- name: 📦 Install dependencies with uv
run: |
uv sync
- name: 🧪 Run Unit Tests
run: |
source .venv/bin/activate
pytest tests/core/netease/test_controller.py
pytest tests/core/netease/test_services.py

build-push:
name: 🚀 Build and Push Docker Images
runs-on: ubuntu-latest
needs: test
steps:
- name: 🐳 Checkout Code
uses: actions/checkout@v3

- name: 🏗️ Build Image
run: |
docker build -f docker/net.dockerfile -t ${{ secrets.DOCKER_USERNAME }}/netease:latest .
docker build -f docker/streamlit.dockerfile -t ${{ secrets.DOCKER_USERNAME }}/streamlit:latest .
- name: 🔐 Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 🚀 Push Image
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker tag ${{ secrets.DOCKER_USERNAME }}/netease:latest alaricle/netease:latest
docker push alaricle/netease:latest
docker tag ${{ secrets.DOCKER_USERNAME }}/streamlit:latest alaricle/streamlit:latest
docker push alaricle/streamlit:latest
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__pycache__
packages/dev_ui/src/dev_ui/common/config/__pycache__/
.venv/
venv/
.cache/
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies = [
"pydantic>=2.10.6",
"pydantic-extra-types[all]>=2.10.2",
"pydantic-settings>=2.8.0",
"pytest>=8.3.5",
"pytest-asyncio>=0.26.0",
]

[project.scripts]
Expand Down Expand Up @@ -88,7 +90,7 @@ addopts = [
]
cache_dir = ".cache/pytest"
doctest_optionflags = "NUMBER IGNORE_EXCEPTION_DETAIL"
markers = ["slow: mark tests as slow"]
markers = ["slow: mark tests as slow", "asyncio: mark tests as async"]
testpaths = ["tests"]
verbosity_assertions = 2
xfail_strict = true
Expand Down
44 changes: 25 additions & 19 deletions src/cat/core/net_ease/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@

from cat.core.net_ease.dto import SalaryOutput
from cat.core.net_ease.services import (
calculate_tax,
calculate_insurance,
calculate_personal_deduction,
calculate_tax,
)


if TYPE_CHECKING:
from fastapi import UploadFile

from cat.core.net_ease.constants import TaxConfig


def handle_convert_gross_to_net(
gross_salary: float, number_of_dependents: int, region: int, tax_config_dep: TaxConfig
gross_salary: float,
number_of_dependents: int,
region: int,
tax_config_dep: TaxConfig,
) -> SalaryOutput:
"""Convert gross salary to net salary.

Expand Down Expand Up @@ -64,14 +66,16 @@ async def handle_upload_excel(file: UploadFile, tax_config_dep: TaxConfig) -> Wo
# Prepare a new workbook to write the result to
new_wb = Workbook()
new_sheet = new_wb.active
new_sheet.append([
"ID",
"Employee Name",
"Gross Salary",
"Number of Dependents",
"Region",
"Net Salary",
])
new_sheet.append(
[
"ID",
"Employee Name",
"Gross Salary",
"Number of Dependents",
"Region",
"Net Salary",
]
)

# Iterate through the rows in the original sheet
for row in sheet.iter_rows(min_row=2, values_only=True):
Expand All @@ -92,13 +96,15 @@ async def handle_upload_excel(file: UploadFile, tax_config_dep: TaxConfig) -> Wo
)

# Write the result to the new sheet
new_sheet.append([
employee_id,
employee_name,
gross_salary,
number_of_dependents,
region,
net_salary_output.net_salary,
])
new_sheet.append(
[
employee_id,
employee_name,
gross_salary,
number_of_dependents,
region,
net_salary_output.net_salary,
]
)

return new_wb
90 changes: 90 additions & 0 deletions tests/core/netease/test_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from io import BytesIO

import pytest
from fastapi import UploadFile
from openpyxl import Workbook
from pytest_mock import MockerFixture

from cat.core.net_ease.constants import TaxBracket, TaxConfig
from cat.core.net_ease.controller import (
handle_convert_gross_to_net,
handle_upload_excel,
)
from cat.core.net_ease.dto import SalaryOutput

tax_config = TaxConfig(
BRACKETS=[
TaxBracket(limit=5_000_000, rate=0.05),
TaxBracket(limit=10_000_000, rate=0.10),
TaxBracket(limit=18_000_000, rate=0.15),
TaxBracket(limit=32_000_000, rate=0.20),
TaxBracket(limit=52_000_000, rate=0.25),
TaxBracket(limit=80_000_000, rate=0.30),
TaxBracket(limit=float("inf"), rate=0.35),
]
)


def test_handle_convert_gross_to_net(mocker: MockerFixture):
mock_insurance = mocker.patch("cat.core.net_ease.controller.calculate_insurance")
mock_insurance.return_value = 10_000_000
mock_deduction = mocker.patch(
"cat.core.net_ease.controller.calculate_personal_deduction"
)
mock_deduction.return_value = 5_000_000
mock_tax = mocker.patch("cat.core.net_ease.controller.calculate_tax")
mock_tax.return_value = 2_000_000

output = handle_convert_gross_to_net(
gross_salary=20_000_000,
number_of_dependents=2,
region=1,
tax_config_dep=tax_config,
)

assert isinstance(output, SalaryOutput)
assert output.gross_salary == 20_000_000
assert output.net_salary == 8_000_000
assert output.insurance_amount == 10_000_000
assert output.personal_income_tax == 2_000_000


@pytest.mark.asyncio
async def test_handle_upload_excel(tmp_path):
# Tạo workbook giả
wb = Workbook()
ws = wb.active
ws.append(["ID", "Employee Name", "Gross Salary", "Number of Dependents", "Region"])
ws.append([1, "Alice", 20000000, 2, 1])
ws.append([2, "Bob", 30000000, 1, 2])

# Lưu vào bytes
file_stream = BytesIO()
wb.save(file_stream)
file_stream.seek(0)

# Tạo UploadFile giả
upload_file = UploadFile(
filename="tests/data_test/data_test_gross_net.xlsx", file=file_stream
)

# Gọi hàm upload
result_wb = await handle_upload_excel(upload_file, tax_config)

assert isinstance(result_wb, Workbook)

result_sheet = result_wb.active
rows = list(result_sheet.iter_rows(values_only=True))

# Header + 2 nhân viên
assert len(rows) == 3
assert rows[0] == (
"ID",
"Employee Name",
"Gross Salary",
"Number of Dependents",
"Region",
"Net Salary",
)
assert rows[1][1] == "Alice"
assert isinstance(rows[1][-1], float) # Net Salary
65 changes: 65 additions & 0 deletions tests/core/netease/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pytest

from cat.core.net_ease.constants import ConstantsSalary, TaxConfig
from cat.core.net_ease.services import (
calculate_insurance,
calculate_personal_deduction,
calculate_tax,
)


def test_calculate_personal_deduction() -> None:
assert calculate_personal_deduction(0) == ConstantsSalary.BASIC_DEDUCTION
assert (
calculate_personal_deduction(2)
== ConstantsSalary.BASIC_DEDUCTION + 2 * ConstantsSalary.DEPENDENT_DEDUCTION
)


@pytest.mark.parametrize(
"gross_salary, region, expected",
[
(
20_000_000,
1,
pytest.approx(
min(20_000_000, ConstantsSalary.LIMIT_BH) * ConstantsSalary.BHXH_RATE
+ min(20_000_000, ConstantsSalary.LIMIT_BH) * ConstantsSalary.BHYT_RATE
+ min(20_000_000, ConstantsSalary.LIMIT_BHTN_V1)
* ConstantsSalary.BHTN_RATE
),
),
(
100_000_000,
2,
pytest.approx(
ConstantsSalary.LIMIT_BH * ConstantsSalary.BHXH_RATE
+ ConstantsSalary.LIMIT_BH * ConstantsSalary.BHYT_RATE
+ ConstantsSalary.LIMIT_BHTN_V2 * ConstantsSalary.BHTN_RATE
),
),
],
)
def test_calculate_insurance(gross_salary, region, expected):
assert calculate_insurance(gross_salary, region) == expected


def test_calculate_tax():
tax_config = TaxConfig()

# Case: 4,000,000 income → 5% bracket only
assert calculate_tax(4_000_000, tax_config) == 4_000_000 * 0.05

# Case: 10,000,000 income → spans 5% + 10%
expected_tax = 5_000_000 * 0.05 + (10_000_000 - 5_000_000) * 0.10
assert calculate_tax(10_000_000, tax_config) == expected_tax

# Case: 50,000,000 income → spans multiple brackets
expected_tax = (
5_000_000 * 0.05
+ 5_000_000 * 0.10
+ 8_000_000 * 0.15
+ 14_000_000 * 0.20
+ (50_000_000 - 32_000_000) * 0.25
)
assert calculate_tax(50_000_000, tax_config) == expected_tax
19 changes: 18 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.