init
This commit is contained in:
405
.gitignore
vendored
Normal file
405
.gitignore
vendored
Normal file
@@ -0,0 +1,405 @@
|
||||
# Visual Studio and .NET Core
|
||||
## Build results
|
||||
.vs/
|
||||
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/git/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/git/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
# JetBrains IDEs
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# AI assistant files
|
||||
.qwen.md
|
||||
.agent/
|
||||
.qwen/
|
||||
.opencode/
|
||||
.claude/
|
||||
.gemini/
|
||||
.kilocode/
|
||||
openspec/changes/
|
||||
|
||||
# Local output folder
|
||||
output/
|
||||
198
AGENTS.md
Normal file
198
AGENTS.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-02-09
|
||||
**Commit:** 107722278e9274073236e8ee4cba21f832b2ed86
|
||||
**Branch:** main
|
||||
|
||||
## OVERVIEW
|
||||
.NET 10.0 console application for extracting employee tax data from PostgreSQL multi-schema databases. Uses static service classes with synchronous operations.
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
```
|
||||
./
|
||||
├── excel_pajak/ # Main application
|
||||
│ ├── Models/ # Data models (EmployeeInfo)
|
||||
│ ├── Services/ # Static services (DatabaseService, ConfigurationService)
|
||||
│ ├── Examples/ # Code examples
|
||||
│ ├── appsettings.json # Configuration
|
||||
│ └── excel_pajak.csproj # Main project
|
||||
├── excel_pajak_test/ # MSTest project
|
||||
└── openspec/ # OpenSpec change management artifacts
|
||||
```
|
||||
|
||||
## COMMANDS
|
||||
|
||||
### Build
|
||||
```bash
|
||||
# Build entire solution
|
||||
dotnet build
|
||||
|
||||
# Build specific project
|
||||
dotnet build excel_pajak
|
||||
dotnet build excel_pajak_test
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run single test class
|
||||
dotnet test --filter "FullyQualifiedName~DatabaseServiceTests"
|
||||
|
||||
# Run single test method
|
||||
dotnet test --filter "FullyQualifiedName~DatabaseServiceTests.ExecuteScalar_NullConnectionString_ThrowsArgumentException"
|
||||
|
||||
# Run tests with specific logger
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
|
||||
# Run tests without building
|
||||
dotnet test --no-build
|
||||
```
|
||||
|
||||
### Run Application
|
||||
```bash
|
||||
# Run with default project
|
||||
dotnet run --project excel_pajak
|
||||
|
||||
# Run with arguments
|
||||
dotnet run --project excel_pajak -- --schema=company_2024 --year=2024
|
||||
```
|
||||
|
||||
### Clean and Rebuild
|
||||
```bash
|
||||
# Clean solution
|
||||
dotnet clean
|
||||
|
||||
# Clean and rebuild
|
||||
dotnet clean && dotnet build -c Release
|
||||
```
|
||||
|
||||
## CODE STYLE GUIDELINES
|
||||
|
||||
### General Principles
|
||||
- Target .NET 10.0 with latest C# features
|
||||
- Enable `ImplicitUsings` and `Nullable` reference types
|
||||
- No comments unless required for complex logic
|
||||
|
||||
### Imports
|
||||
- Use file-scoped namespaces (`namespace excel_pajak.Services;`)
|
||||
- Group imports: System namespaces first, then third-party
|
||||
- Order: `System.*` → `Microsoft.*` → `ThirdParty.*` → `Project.*`
|
||||
```csharp
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
|
||||
namespace excel_pajak.Services;
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- **Classes**: PascalCase (`DatabaseService`, `EmployeeInfo`)
|
||||
- **Methods**: PascalCase (`ExecuteScalar`, `Initialize`)
|
||||
- **Properties**: PascalCase (`EmployeeNumber`, `ConnectionString`)
|
||||
- **Local variables**: camelCase (`connectionString`, `result`)
|
||||
- **Constants**: UPPER_SNAKE_CASE for values, PascalCase for statics
|
||||
- **Private fields**: camelCase with underscore prefix (`_connectionString`)
|
||||
- **Parameters**: camelCase (`schema`, `year`)
|
||||
|
||||
### Types
|
||||
- Use nullable reference types (`string?`, `int?`)
|
||||
- Prefer `var` when type is obvious from context
|
||||
- Use explicit types for public APIs and method returns
|
||||
- Use `string?` instead of `string` with null annotations for nullable properties
|
||||
|
||||
### Error Handling
|
||||
- Validate arguments with `ArgumentException` for null/empty checks
|
||||
- Wrap database exceptions with `InvalidOperationException`
|
||||
- Use result tuples for recoverable errors: `(bool success, string? result, Exception? error)`
|
||||
- Re-throw exceptions with context using `throw new ExceptionType("message", inner)`
|
||||
```csharp
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString));
|
||||
|
||||
try { /* ... */ }
|
||||
catch (NpgsqlException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Database error: {ex.Message}", ex);
|
||||
}
|
||||
```
|
||||
|
||||
### Static Service Pattern
|
||||
- Services use static classes with `Initialize()` pattern
|
||||
- Lazy initialization for dependencies
|
||||
- No async database operations (intentional - see ANTI-PATTERNS)
|
||||
```csharp
|
||||
public static class DatabaseService
|
||||
{
|
||||
private static string? _connectionString;
|
||||
|
||||
public static void Initialize(string connectionString)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Serialization
|
||||
- Use `System.Text.Json` with `[JsonPropertyName]` attributes
|
||||
- Snake_case for JSON property names
|
||||
```csharp
|
||||
public class EmployeeInfo
|
||||
{
|
||||
[JsonPropertyName("employee_number")]
|
||||
public string? EmployeeNumber { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration
|
||||
- Use `appsettings.json` for all configuration
|
||||
- Access via `Microsoft.Extensions.Configuration`
|
||||
- Bind options with `ConfigurationBinder.Bind()`
|
||||
|
||||
### Validation
|
||||
- Schema names: `^[a-zA-Z0-9_-]+$`
|
||||
- Years: `^\d{4}$`
|
||||
```csharp
|
||||
private static readonly Regex SchemaValidation = new(@"^[a-zA-Z0-9_-]+$", RegexOptions.Compiled);
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
- Synchronous only using Npgsql
|
||||
- Use `json_agg(row_to_json(t))` for PostgreSQL JSON aggregation
|
||||
- Placeholder replacement: `query.Replace("{schema}", schema)`
|
||||
- Dispose resources with `using` statements or `using var`
|
||||
|
||||
### Testing
|
||||
- Use MSTest with `[TestClass]` and `[TestMethod]`
|
||||
- Use Shouldly for assertions: `Should.Throw<>`, `ShouldBe()`, `ShouldBeNull()`
|
||||
- Use Moq for mocking
|
||||
- Mark integration tests with `[Ignore]` if they require database connection
|
||||
- Use region blocks (`#region`) for grouping related tests
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location |
|
||||
|------|----------|
|
||||
| Database operations | `excel_pajak/Services/DatabaseService.cs` |
|
||||
| Configuration handling | `excel_pajak/Services/ConfigurationService.cs` |
|
||||
| Data models | `excel_pajak/Models/EmployeeInfo.cs` |
|
||||
| Tests | `excel_pajak_test/DatabaseServiceTests.cs` |
|
||||
| Configuration | `excel_pajak/appsettings.json` |
|
||||
|
||||
## ANTI-PATTERNS TO AVOID
|
||||
- No async database operations (intentional design, may need future migration)
|
||||
- No connection pooling or disposal tracking in services
|
||||
- Static state requires explicit `Initialize()` before use
|
||||
|
||||
## KEY DEPENDENCIES
|
||||
- **Npgsql**: PostgreSQL driver (sync operations)
|
||||
- **NPOI**: Excel file manipulation
|
||||
- **Microsoft.Extensions.Configuration**: Configuration management
|
||||
- **Shouldly**: Test assertions
|
||||
- **Moq**: Mocking framework
|
||||
- **MSTest**: Test framework
|
||||
|
||||
## NOTES
|
||||
- All tests use MSTest framework
|
||||
- Configuration requires valid connection string in `appsettings.json`
|
||||
- Schema and Year parameters drive query execution
|
||||
- Connection strings validated with regex for security
|
||||
103
QWEN.md
Normal file
103
QWEN.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Excel Pajak Application
|
||||
|
||||
## Project Overview
|
||||
|
||||
Excel Pajak is a .NET 10.0 console application that connects to a PostgreSQL database to handle tax-related data processing. The application is designed to execute database queries and process employee information, with a focus on connecting to specific database schemas containing employee data.
|
||||
|
||||
### Architecture
|
||||
- **Main Application**: Console application in the `excel_pajak` directory
|
||||
- **Test Project**: Unit and integration tests in the `excel_pajak_test` directory
|
||||
- **Technology Stack**:
|
||||
- .NET 10.0
|
||||
- PostgreSQL database connectivity via Npgsql
|
||||
- Microsoft.Extensions.Configuration for configuration management
|
||||
- MSTest for unit testing
|
||||
- Moq for mocking dependencies
|
||||
- Shouldly for assertions
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **DatabaseService** (`Services/DatabaseService.cs`): Handles PostgreSQL connections and executes scalar queries
|
||||
2. **EmployeeInfo Model** (`Models/EmployeeInfo.cs`): Represents employee data structure with employee number and schema
|
||||
3. **Configuration**: Managed through `appsettings.json` with connection strings and application settings
|
||||
4. **Examples**: Demonstrates JSON deserialization of employee data
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- .NET 10.0 SDK
|
||||
- PostgreSQL database server (version compatible with Npgsql 10.0.1)
|
||||
- Access to the configured database (default connection points to localhost:55432)
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Restore dependencies
|
||||
dotnet restore
|
||||
|
||||
# Build the application
|
||||
dotnet build
|
||||
|
||||
# Run the application
|
||||
dotnet run --project excel_pajak/excel_pajak.csproj
|
||||
|
||||
# Run tests
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### Configuration
|
||||
The application uses `appsettings.json` for configuration:
|
||||
- Connection string for PostgreSQL database
|
||||
- Schema list (currently contains "_onx4pzkwkeortehfjthgyfkb7c")
|
||||
- Year setting (currently "2025")
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Coding Standards
|
||||
- Uses nullable reference types (`#nullable enable`)
|
||||
- Follows .NET naming conventions
|
||||
- Implements asynchronous programming patterns where appropriate
|
||||
- Includes proper error handling with specific exception types
|
||||
|
||||
### Testing Approach
|
||||
- Unit tests for business logic with mocked dependencies
|
||||
- Integration tests that connect to actual PostgreSQL database
|
||||
- Tests cover both positive and negative scenarios
|
||||
- Uses Shouldly for readable assertions
|
||||
|
||||
### Error Handling
|
||||
- Validates input parameters and throws appropriate exceptions
|
||||
- Handles database connection errors gracefully
|
||||
- Provides detailed error messages for debugging
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
excel_pajak/
|
||||
├── excel_pajak/ # Main application
|
||||
│ ├── Models/ # Data models
|
||||
│ │ └── EmployeeInfo.cs # Employee data structure
|
||||
│ ├── Services/ # Business logic services
|
||||
│ │ └── DatabaseService.cs # PostgreSQL database operations
|
||||
│ ├── Examples/ # Usage examples
|
||||
│ │ └── EmployeeJsonExample.cs # JSON processing example
|
||||
│ ├── Program.cs # Application entry point
|
||||
│ ├── appsettings.json # Configuration file
|
||||
│ └── excel_pajak.csproj # Project file
|
||||
├── excel_pajak_test/ # Test project
|
||||
│ ├── DatabaseServiceTests.cs # Unit tests
|
||||
│ ├── DatabaseServiceIntegrationTests.cs # Integration tests
|
||||
│ └── excel_pajak_test.csproj # Test project file
|
||||
└── openspec/ # OpenAPI specification directory
|
||||
```
|
||||
|
||||
## Database Connection
|
||||
The application connects to PostgreSQL using Npgsql with the connection string defined in `appsettings.json`. The default configuration connects to:
|
||||
- Server: localhost
|
||||
- Port: 55432
|
||||
- Database: andal_kharisma
|
||||
- User: postgres
|
||||
- Password: Release@2024
|
||||
|
||||
## Testing
|
||||
The project includes both unit tests (with mocked dependencies) and integration tests (requiring a live database connection). Unit tests validate the logic without external dependencies, while integration tests verify actual database connectivity and query execution.
|
||||
|
||||
To run integration tests, ensure that a PostgreSQL database is accessible with the configured connection string.
|
||||
164
README.md
Normal file
164
README.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Excel Pajak
|
||||
|
||||
Employee tax data extraction and Excel report generation tool for Indonesian payroll processing.
|
||||
|
||||
## Overview
|
||||
|
||||
.NET 10.0 console application that extracts employee tax data from PostgreSQL multi-schema databases and generates Excel reports using NPOI library.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 10.0 SDK or later
|
||||
- PostgreSQL database
|
||||
- Valid connection string in configuration
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
excel_pajak/
|
||||
├── Models/ # Data models (EmployeeInfo, EmployeePayrollInfo)
|
||||
├── Services/ # Static services (DatabaseService, ExcelService)
|
||||
├── Examples/ # Code examples
|
||||
├── appsettings.json # Application configuration
|
||||
└── excel_pajak.csproj # Main project
|
||||
|
||||
excel_pajak_test/ # MSTest unit tests
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit [`excel_pajak/appsettings.json`](excel_pajak/appsettings.json) to configure:
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=payroll;Username=your_user;Password=your_password"
|
||||
},
|
||||
"ExcelSettings": {
|
||||
"TemplatePath": "simulasi_perhitungan_ak.xlsx",
|
||||
"OutputDirectory": "Output"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Build entire solution
|
||||
dotnet build
|
||||
|
||||
# Build specific project
|
||||
dotnet build excel_pajak
|
||||
dotnet build excel_pajak_test
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# Run with default settings
|
||||
dotnet run --project excel_pajak
|
||||
|
||||
# Run with command-line arguments
|
||||
dotnet run --project excel_pajak -- --schema=company_2024 --year=2024
|
||||
|
||||
# Run specific schema and year
|
||||
dotnet run --project excel_pajak -- --schema=company_2025 --year=2025
|
||||
```
|
||||
|
||||
### Command-Line Arguments
|
||||
|
||||
| Argument | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `--schema` | Database schema name | Yes |
|
||||
| `--year` | Tax year (4 digits) | Yes |
|
||||
| `--help` | Show help information | No |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run specific test class
|
||||
dotnet test --filter "FullyQualifiedName~DatabaseServiceTests"
|
||||
|
||||
# Run specific test method
|
||||
dotnet test --filter "FullyQualifiedName~DatabaseServiceTests.ExecuteScalar_NullConnectionString_ThrowsArgumentException"
|
||||
|
||||
# Run with detailed output
|
||||
dotnet test --logger "console;verbosity=detailed"
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Configuration Phase
|
||||
- Load connection string from `appsettings.json`
|
||||
- Validate schema name and year parameters
|
||||
- Initialize DatabaseService with connection string
|
||||
|
||||
### 2. Data Extraction Phase
|
||||
- Connect to PostgreSQL database
|
||||
- Execute query to extract employee tax data
|
||||
- Query uses `json_agg(row_to_json(t))` for JSON aggregation
|
||||
- Placeholder replacement: `{schema}` in query template
|
||||
|
||||
### 3. Excel Generation Phase
|
||||
- Load template Excel file (`simulasi_perhitungan_ak.xlsx`)
|
||||
- Write employee data to specific cells
|
||||
- Optionally refresh formulas
|
||||
- Save to Output directory with naming convention: `{EmployeeNumber}_{Year}.xlsx`
|
||||
|
||||
### 4. Output Phase
|
||||
- Generate Excel files in `Output/` directory
|
||||
- Each file named after employee number and year
|
||||
- Example: `AA001_2025.xlsx`
|
||||
|
||||
## Key Services
|
||||
|
||||
### DatabaseService
|
||||
- Static service for PostgreSQL operations
|
||||
- Synchronous database operations using Npgsql
|
||||
- Query execution with schema placeholders
|
||||
|
||||
### ExcelService
|
||||
- Excel file manipulation using NPOI
|
||||
- Template loading and data writing
|
||||
- Formula refresh support
|
||||
- File overwrite control
|
||||
|
||||
### ConfigurationService
|
||||
- Manages application settings
|
||||
- Loads configuration from appsettings.json
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Pattern**: Static service classes with Initialize()
|
||||
- **Database**: Synchronous operations (no async)
|
||||
- **Serialization**: System.Text.Json with snake_case
|
||||
- **Validation**: Regex-based schema/year validation
|
||||
|
||||
## Anti-Patterns Avoided
|
||||
|
||||
- No async database operations (synchronous by design)
|
||||
- No connection pooling in services
|
||||
- Static state requires explicit Initialize() before use
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
- Verify PostgreSQL server is running
|
||||
- Check connection string in appsettings.json
|
||||
- Ensure database user has appropriate permissions
|
||||
|
||||
### Schema Not Found
|
||||
- Verify schema name exists in database
|
||||
- Schema names should match pattern: `^[a-zA-Z0-9_-]+$`
|
||||
|
||||
### Excel Output Issues
|
||||
- Ensure Output directory exists
|
||||
- Check write permissions
|
||||
- Verify template file exists
|
||||
|
||||
## License
|
||||
|
||||
Internal use only.
|
||||
4
excel_pajak.slnx
Normal file
4
excel_pajak.slnx
Normal file
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="excel_pajak/excel_pajak.csproj" />
|
||||
<Project Path="excel_pajak_test/excel_pajak_test.csproj" />
|
||||
</Solution>
|
||||
78
excel_pajak/Examples/EmployeeJsonExample.cs
Normal file
78
excel_pajak/Examples/EmployeeJsonExample.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json;
|
||||
using excel_pajak.Models;
|
||||
|
||||
namespace excel_pajak.Examples;
|
||||
|
||||
public static class EmployeeJsonExample
|
||||
{
|
||||
public static void RunExample()
|
||||
{
|
||||
// Sample JSON from PostgreSQL query
|
||||
var json = @"[
|
||||
{""employee_number"":""PS2MA001"",""schema"":""_onx4pzkwkeortehfjthgyfkb7c""},
|
||||
{""employee_number"":""PS2MA002"",""schema"":""_onx4pzkwkeortehfjthgyfkb7c""},
|
||||
{""employee_number"":""PS2MA003"",""schema"":""_onx4pzkwkeortehfjthgyfkb7c""}
|
||||
]";
|
||||
|
||||
// Configure options
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Deserialize JSON array to EmployeeInfo[]
|
||||
var employees = JsonSerializer.Deserialize<EmployeeInfo[]>(json, options);
|
||||
|
||||
if (employees != null)
|
||||
{
|
||||
Console.WriteLine($"Deserialized {employees.Length} employees:");
|
||||
foreach (var employee in employees)
|
||||
{
|
||||
Console.WriteLine($" - {employee.EmployeeNumber} (schema: {employee.Schema})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"JSON deserialization error: {ex.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
|
||||
public static void TestEmptyArray()
|
||||
{
|
||||
// Test empty array
|
||||
var emptyJson = "[]";
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
var employees = JsonSerializer.Deserialize<EmployeeInfo[]>(emptyJson, options);
|
||||
Console.WriteLine($"Empty array test: {(employees?.Length == 0 ? "PASS" : "FAIL")}");
|
||||
}
|
||||
|
||||
public static void TestMalformedJson()
|
||||
{
|
||||
// Test malformed JSON
|
||||
var malformedJson = "[{invalid json}]";
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var employees = JsonSerializer.Deserialize<EmployeeInfo[]>(malformedJson, options);
|
||||
Console.WriteLine("Malformed JSON test: FAIL - should have thrown exception");
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Console.WriteLine("Malformed JSON test: PASS - exception thrown as expected");
|
||||
}
|
||||
}
|
||||
}
|
||||
13
excel_pajak/Models/AppSettings.cs
Normal file
13
excel_pajak/Models/AppSettings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace excel_pajak.Models;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public List<string> SchemaList { get; set; } = new List<string>();
|
||||
|
||||
[RegularExpression(@"^\d{4}$", ErrorMessage = "Year must be a 4-digit number")]
|
||||
public string Year { get; set; } = string.Empty;
|
||||
|
||||
public ExcelSettings? ExcelSettings { get; set; }
|
||||
}
|
||||
12
excel_pajak/Models/EmployeeInfo.cs
Normal file
12
excel_pajak/Models/EmployeeInfo.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace excel_pajak.Models;
|
||||
|
||||
public class EmployeeInfo
|
||||
{
|
||||
[JsonPropertyName("employee_number")]
|
||||
public string? EmployeeNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("schema")]
|
||||
public string? Schema { get; set; }
|
||||
}
|
||||
106
excel_pajak/Models/EmployeePayrollInfo.cs
Normal file
106
excel_pajak/Models/EmployeePayrollInfo.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace excel_pajak.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents employee payroll and tax data for a specific payment period.
|
||||
/// Used for extracting tax-related information from PostgreSQL databases.
|
||||
/// </summary>
|
||||
public class EmployeePayrollInfo
|
||||
{
|
||||
[JsonPropertyName("employee_number")]
|
||||
public string? EmployeeNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("employee_name")]
|
||||
public string? EmployeeName { get; set; }
|
||||
|
||||
[JsonPropertyName("employee_npwp")]
|
||||
public string? EmployeeNpwp { get; set; }
|
||||
|
||||
[JsonPropertyName("tax_marital_status")]
|
||||
public string? TaxMaritalStatus { get; set; }
|
||||
|
||||
[JsonPropertyName("employee_tax_status")]
|
||||
public int EmployeeTaxStatus { get; set; }
|
||||
|
||||
[JsonPropertyName("payment_period")]
|
||||
public string? PaymentPeriod { get; set; }
|
||||
|
||||
[JsonPropertyName("npwp_pemotong")]
|
||||
public string? NpwpPemotong { get; set; }
|
||||
|
||||
[JsonPropertyName("tax_type")]
|
||||
public string? TaxType { get; set; }
|
||||
|
||||
[JsonPropertyName("basic_salary")]
|
||||
public decimal BasicSalary { get; set; }
|
||||
|
||||
[JsonPropertyName("overtime")]
|
||||
public decimal Overtime { get; set; }
|
||||
|
||||
[JsonPropertyName("allowance")]
|
||||
public decimal Allowance { get; set; }
|
||||
|
||||
[JsonPropertyName("deduction")]
|
||||
public decimal Deduction { get; set; }
|
||||
|
||||
[JsonPropertyName("benefit_in_kind")]
|
||||
public decimal BenefitInKind { get; set; }
|
||||
|
||||
[JsonPropertyName("severance")]
|
||||
public decimal Severance { get; set; }
|
||||
|
||||
[JsonPropertyName("jkk")]
|
||||
public decimal Jkk { get; set; }
|
||||
|
||||
[JsonPropertyName("jkm")]
|
||||
public decimal Jkm { get; set; }
|
||||
|
||||
[JsonPropertyName("bpjs_kesehatan_company")]
|
||||
public decimal BpjsKesehatanCompany { get; set; }
|
||||
|
||||
[JsonPropertyName("jht_emp")]
|
||||
public decimal JhtEmp { get; set; }
|
||||
|
||||
[JsonPropertyName("jp_emp")]
|
||||
public decimal JpEmp { get; set; }
|
||||
|
||||
[JsonPropertyName("pension_emp")]
|
||||
public decimal PensionEmp { get; set; }
|
||||
|
||||
[JsonPropertyName("employee_condition")]
|
||||
public int EmployeeCondition { get; set; }
|
||||
|
||||
[JsonPropertyName("resign_type")]
|
||||
public string? ResignType { get; set; }
|
||||
|
||||
[JsonPropertyName("allowance_tax_regular")]
|
||||
public decimal AllowanceTaxRegular { get; set; }
|
||||
|
||||
[JsonPropertyName("tax_regular")]
|
||||
public decimal TaxRegular { get; set; }
|
||||
|
||||
[JsonPropertyName("allowance_irregular")]
|
||||
public decimal AllowanceIrregular { get; set; }
|
||||
|
||||
[JsonPropertyName("benefit_in_kind_irregular")]
|
||||
public decimal BenefitInKindIrregular { get; set; }
|
||||
|
||||
[JsonPropertyName("allowance_tax_irregular")]
|
||||
public decimal AllowanceTaxIrregular { get; set; }
|
||||
|
||||
[JsonPropertyName("tax_irregular")]
|
||||
public decimal TaxIrregular { get; set; }
|
||||
|
||||
[JsonPropertyName("severance_tax")]
|
||||
public decimal SeveranceTax { get; set; }
|
||||
|
||||
[JsonPropertyName("allowance_severance_tax")]
|
||||
public decimal AllowanceSeveranceTax { get; set; }
|
||||
|
||||
[JsonPropertyName("beginning_salary_netto")]
|
||||
public decimal BeginningSalaryNetto { get; set; }
|
||||
|
||||
[JsonPropertyName("beginning_pph21")]
|
||||
public decimal BeginningPph21 { get; set; }
|
||||
}
|
||||
7
excel_pajak/Models/ExcelSettings.cs
Normal file
7
excel_pajak/Models/ExcelSettings.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace excel_pajak.Models;
|
||||
|
||||
public class ExcelSettings
|
||||
{
|
||||
public string TemplatePath { get; set; } = string.Empty;
|
||||
public string OutputFolder { get; set; } = string.Empty;
|
||||
}
|
||||
203
excel_pajak/Program.cs
Normal file
203
excel_pajak/Program.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using excel_pajak.Services;
|
||||
using excel_pajak.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace excel_pajak;
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// Initialize configuration
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
|
||||
IConfigurationRoot configuration = builder.Build();
|
||||
var schemaList = configuration.GetSection("SchemaList").Get<List<string>>() ?? [];
|
||||
var year = configuration.GetValue<string>("Year") ?? string.Empty;
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection") ?? string.Empty;
|
||||
var outputFolder = configuration.GetValue<string>("ExcelSettings:OutputFolder") ?? string.Empty;
|
||||
var templatePath = configuration.GetValue<string>("ExcelSettings:TemplatePath") ?? string.Empty;
|
||||
|
||||
|
||||
|
||||
|
||||
// Process each schema in the configuration
|
||||
var allEmployeeInfos = new List<EmployeeInfo>();
|
||||
|
||||
foreach (var schema in schemaList)
|
||||
{
|
||||
Console.WriteLine($"\nProcessing schema: {schema}");
|
||||
string query =
|
||||
$"""
|
||||
select json_agg(row_to_json(t)) from (
|
||||
select distinct e."NIK" as employee_number , '{schema}' as schema
|
||||
from {schema}."EmployeeReportPeriodics" erp
|
||||
inner join {schema}."Employees" e on erp."EmployeeId" =e."Id"
|
||||
inner join {schema}."EmployeeTaxStatuses" ets on e."EmployeeTaxStatusRefId" =ets."Id"
|
||||
where erp."PaymentPeriod" between '{year}01' and '{year}12'
|
||||
and ets."EmployeeTaxStatusCode" =1
|
||||
and erp."TaxCalculationType" <>1 AND (
|
||||
e."LastWorkingDate" IS NULL
|
||||
OR e."LastWorkingDate" >= extract(epoch from DATE '{year}-01-01')*1000
|
||||
)
|
||||
) t
|
||||
""";
|
||||
|
||||
var result = DatabaseService.ExecuteScalar(query, connectionString);
|
||||
if (result != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the JSON result to a list of EmployeeInfo objects
|
||||
var employeeInfos = JsonSerializer.Deserialize<List<EmployeeInfo>>(result);
|
||||
|
||||
if (employeeInfos != null)
|
||||
{
|
||||
// Add the retrieved employees to the overall list
|
||||
allEmployeeInfos.AddRange(employeeInfos);
|
||||
Console.WriteLine($"Retrieved {employeeInfos.Count} employee records from schema '{schema}'");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"Error deserializing JSON from schema '{schema}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"No data returned from schema '{schema}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Output the aggregated employee information to the console
|
||||
Console.WriteLine($"\nAggregated {allEmployeeInfos.Count} employee records from all schemas:");
|
||||
foreach (var employeeInfo in allEmployeeInfos)
|
||||
{
|
||||
Console.WriteLine($"Employee Number: {employeeInfo.EmployeeNumber}, Schema: {employeeInfo.Schema}");
|
||||
ExcelService.InitExcel($"{employeeInfo.EmployeeNumber}_{year}.xlsx", templatePath, outputFolder);
|
||||
string queryEmployeeData = $"""
|
||||
select json_agg(row_to_json(t)) from (
|
||||
select e."NIK" as employee_number, e."Name" as employee_name,e."NPWP" as employee_npwp,
|
||||
(case
|
||||
when erp."TaxStatus"=1 then 'T0'
|
||||
when erp."TaxStatus"=2 then 'T1'
|
||||
when erp."TaxStatus"=3 then 'T2'
|
||||
when erp."TaxStatus"=4 then 'T3'
|
||||
when erp."TaxStatus"=5 then 'K0'
|
||||
when erp."TaxStatus"=6 then 'K1'
|
||||
when erp."TaxStatus"=7 then 'K2'
|
||||
when erp."TaxStatus"=8 then 'K3'
|
||||
end
|
||||
) as tax_marital_status,
|
||||
ets."EmployeeTaxStatusCode" as employee_tax_status,
|
||||
erp."PaymentPeriod" as payment_period,
|
||||
erp."NPWPPemotong" as npwp_pemotong,
|
||||
(case erp."TaxCalculationType" when 2 then 'PaidByEmployee' when 3 then 'PaidAsAllowance' else 'PaidByCompany' end) as tax_type,
|
||||
erp."BasicSalary" as basic_salary,
|
||||
erp."Overtime" as overtime,
|
||||
erp."AllowanceRegular" as allowance,
|
||||
erp."Deductions" as deduction,
|
||||
erp."Natura" as benefit_in_kind,
|
||||
erp."Severance" as severance,
|
||||
erp."JKKCompany" as jkk,
|
||||
erp."JKMCompany" as jkm,
|
||||
erp."BPJSKesCompany" +erp."OtherInsuranceCompany" as bpjs_kesehatan_company,
|
||||
erp."JHTEmployee" as jht_emp,
|
||||
erp."JPEmployee" as jp_emp,
|
||||
0 as pension_emp,
|
||||
erp."EmployeeCondition" as employee_condition,
|
||||
erp."ResignType" as resign_type,
|
||||
erp."AllowanceTaxRegular" as allowance_tax_regular,
|
||||
erp."TaxRegular" as tax_regular ,
|
||||
erp."AllowanceIrregular" as allowance_irregular,
|
||||
0 as benefit_in_kind_irregular,
|
||||
erp."AllowanceTaxIrregular" as allowance_tax_irregular,
|
||||
erp."TaxIrregular" as tax_irregular,
|
||||
0 as severance_tax,
|
||||
0 as allowance_severance_tax,
|
||||
erp."BegSalaryNetto" as beginning_salary_netto,
|
||||
erp."BegPPh21" as beginning_pph21
|
||||
from {employeeInfo.Schema}."EmployeeReportPeriodics" erp
|
||||
inner join {employeeInfo.Schema}."Employees" e on erp."EmployeeId" = e."Id"
|
||||
inner join {employeeInfo.Schema}."EmployeeTaxStatuses" ets on e."EmployeeTaxStatusRefId" =ets."Id"
|
||||
where erp."TaxCalculationType" <>1
|
||||
and erp."PaymentPeriod" between '{year}01' and '{year}12'
|
||||
and e."NIK" ='{employeeInfo.EmployeeNumber}'
|
||||
) t
|
||||
""";
|
||||
string resultPayroll = DatabaseService.ExecuteScalar(queryEmployeeData, connectionString) ?? string.Empty;
|
||||
if (resultPayroll != "")
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the JSON result to a list of EmployeeInfo objects
|
||||
var employeeInfos = JsonSerializer.Deserialize<List<EmployeePayrollInfo>>(resultPayroll);
|
||||
|
||||
if (employeeInfos != null)
|
||||
{
|
||||
string taxTypeBasal = employeeInfos.LastOrDefault(x => x.BasicSalary > 0)?.TaxType ?? "PaidByEmployee";
|
||||
string taxMaritalStatus = employeeInfos.FirstOrDefault()?.TaxMaritalStatus ?? "T0";
|
||||
string periodStart = employeeInfos.OrderBy(x => x.PaymentPeriod).FirstOrDefault()?.PaymentPeriod ?? "";
|
||||
string periodEnd = employeeInfos.OrderBy(x => x.PaymentPeriod).LastOrDefault()?.PaymentPeriod ?? "";
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", taxMaritalStatus, 3, 3);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", int.Parse(periodStart.Substring(4, 2)), 2, 7);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", int.Parse(periodEnd.Substring(4, 2)), 3, 7);
|
||||
List<string?> periodList = employeeInfos.Select(x => x.PaymentPeriod).Distinct().ToList() ?? new();
|
||||
foreach (string period in periodList)
|
||||
{
|
||||
var employeeInfoDetail = employeeInfos.FirstOrDefault(x => x.PaymentPeriod == period && x.TaxType == taxTypeBasal);
|
||||
int month = int.Parse(period?.Substring(4, 2) ?? "01");
|
||||
if (employeeInfoDetail != null)
|
||||
{
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BasicSalary, 6, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Overtime, 8, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Allowance, 7, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", Math.Abs(employeeInfoDetail.Deduction) * -1, 10, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BenefitInKind, 9, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Jkk, 11, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Jkm, 12, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BpjsKesehatanCompany, 13, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.JhtEmp, 18, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.JpEmp, 19, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.PensionEmp, 20, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceTaxRegular, 14, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceIrregular + employeeInfoDetail.BenefitInKindIrregular, 36, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceTaxIrregular, 37, month + 1);
|
||||
}
|
||||
employeeInfoDetail = employeeInfos.FirstOrDefault(x => x.PaymentPeriod == period && x.TaxType != taxTypeBasal);
|
||||
if (employeeInfoDetail != null)
|
||||
{
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BasicSalary, 52, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Overtime, 54, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Allowance, 53, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", Math.Abs(employeeInfoDetail.Deduction) * -1, 56, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BenefitInKind, 55, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Jkk, 57, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.Jkm, 58, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.BpjsKesehatanCompany, 59, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceTaxRegular, 60, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceIrregular + employeeInfoDetail.BenefitInKindIrregular, 85, month + 1);
|
||||
ExcelService.WriteExcel($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx", employeeInfoDetail.AllowanceTaxIrregular, 86, month + 1);
|
||||
}
|
||||
}
|
||||
|
||||
ExcelService.RefreshFormulas($"{outputFolder}\\{employeeInfo.EmployeeNumber}_{year}.xlsx");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"Error deserializing JSON from schema '{employeeInfo.Schema}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"No data returned from schema '{employeeInfo.Schema}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
excel_pajak/Services/AGENTS.md
Normal file
39
excel_pajak/Services/AGENTS.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# SERVICES DIRECTORY
|
||||
|
||||
**Generated:** 2026-02-06
|
||||
**Path:** excel_pajak/Services/
|
||||
|
||||
## OVERVIEW
|
||||
Static service classes for database and configuration operations.
|
||||
|
||||
## FILES
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `DatabaseService.cs` | PostgreSQL query execution with sync operations |
|
||||
| `ConfigurationService.cs` | Configuration access with lazy initialization |
|
||||
|
||||
## CONVENTIONS
|
||||
- Static classes with `Initialize()` pattern for dependency injection
|
||||
- Lazy initialization in ConfigurationService: creates default config on first access
|
||||
- Error handling via result tuples: `(bool success, string? result, Exception? error)`
|
||||
- Schema name validation regex: `^[a-zA-Z0-9_-]+$`
|
||||
- Year validation regex: `^\d{4}$`
|
||||
|
||||
## ERROR HANDLING
|
||||
```csharp
|
||||
// Pattern: TryExecute pattern for schema iteration
|
||||
var (success, result, error) = DatabaseService.ExecuteEmployeeTaxQueryWithErrorHandling(schema, year);
|
||||
if (success) { /* use result */ }
|
||||
else { /* handle error, continue processing */ }
|
||||
```
|
||||
|
||||
## INITIALIZATION
|
||||
```csharp
|
||||
// Before using services:
|
||||
ConfigurationService.Initialize(configuration);
|
||||
DatabaseService.Initialize(connectionString);
|
||||
```
|
||||
|
||||
## QUERY PATTERNS
|
||||
- Use `json_agg(row_to_json(t))` for PostgreSQL JSON aggregation
|
||||
- Placeholder replacement for schema/year: `query.Replace("{schema}", schema)`
|
||||
54
excel_pajak/Services/DatabaseService.cs
Normal file
54
excel_pajak/Services/DatabaseService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Npgsql;
|
||||
|
||||
namespace excel_pajak.Services;
|
||||
|
||||
public static class DatabaseService
|
||||
{
|
||||
|
||||
|
||||
public static string? ExecuteScalar(string query, string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
throw new ArgumentException("Query cannot be null or empty.", nameof(query));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = new NpgsqlConnection(connectionString);
|
||||
connection.Open();
|
||||
|
||||
using var command = new NpgsqlCommand(query, connection);
|
||||
var result = command.ExecuteScalar();
|
||||
connection.Close();
|
||||
// Handle empty result sets (no rows)
|
||||
if (result == null || result == DBNull.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
catch (Npgsql.PostgresException ex)
|
||||
{
|
||||
// Re-throw PostgreSQL exceptions with context
|
||||
throw new InvalidOperationException(
|
||||
$"PostgreSQL error executing query: {ex.MessageText ?? ex.Message}",
|
||||
ex);
|
||||
}
|
||||
catch (NpgsqlException ex)
|
||||
{
|
||||
// Re-throw Npgsql exceptions with context
|
||||
throw new InvalidOperationException(
|
||||
$"Database error executing query: {ex.Message}",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
206
excel_pajak/Services/ExcelService.cs
Normal file
206
excel_pajak/Services/ExcelService.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System.IO;
|
||||
using NPOI.SS.UserModel;
|
||||
using NPOI.XSSF.UserModel;
|
||||
using NPOI.XSSF.UserModel.Helpers;
|
||||
using excel_pajak.Models;
|
||||
|
||||
namespace excel_pajak.Services;
|
||||
|
||||
public static class ExcelService
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an Excel workbook from a template file.
|
||||
/// </summary>
|
||||
/// <param name="outputName">The name of the output Excel file (without .xlsx extension).</param>
|
||||
/// <param name="templatePath">The relative path to the template file from the application base directory.</param>
|
||||
/// <param name="outputFolder">The folder where the output file will be created.</param>
|
||||
/// <param name="overwrite">If true, will overwrite an existing file. If false, will throw IOException if file exists.</param>
|
||||
/// <returns>A tuple containing success status, output file path on success, or exception on failure.</returns>
|
||||
public static (bool success, string? result, Exception? error) InitExcel(string outputName, string templatePath, string outputFolder, bool overwrite = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(outputName))
|
||||
{
|
||||
return (false, null, new ArgumentException("File name cannot be null or empty"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var templateFullPath = Path.IsPathRooted(templatePath)
|
||||
? templatePath
|
||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, templatePath);
|
||||
|
||||
if (!File.Exists(templateFullPath))
|
||||
{
|
||||
return (false, null, new FileNotFoundException($"Template file not found: {templateFullPath}"));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(outputFolder))
|
||||
{
|
||||
Directory.CreateDirectory(outputFolder);
|
||||
}
|
||||
|
||||
var outputFileName = outputName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)
|
||||
? outputName
|
||||
: outputName + ".xlsx";
|
||||
var outputFullPath = Path.Combine(outputFolder, outputFileName);
|
||||
|
||||
if (!overwrite && File.Exists(outputFullPath))
|
||||
{
|
||||
return (false, null, new IOException($"File already exists: {outputFullPath}. Set overwrite=true to replace it."));
|
||||
}
|
||||
|
||||
using (var sourceStream = new FileStream(templateFullPath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
var workbook = new XSSFWorkbook(sourceStream);
|
||||
using (var destinationStream = new FileStream(outputFullPath, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write))
|
||||
{
|
||||
workbook.Write(destinationStream, false);
|
||||
}
|
||||
workbook.Close();
|
||||
}
|
||||
|
||||
return (true, outputFullPath, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool WriteExcel(string fileName, string value, int row, int column)
|
||||
{
|
||||
return InternalWriteExcel(fileName, value, row, column);
|
||||
}
|
||||
|
||||
public static bool WriteExcel(string fileName, long value, int row, int column)
|
||||
{
|
||||
return InternalWriteExcel(fileName, value, row, column);
|
||||
}
|
||||
|
||||
public static bool WriteExcel(string fileName, decimal value, int row, int column)
|
||||
{
|
||||
return InternalWriteExcel(fileName, value, row, column);
|
||||
}
|
||||
|
||||
private static bool InternalWriteExcel(string fileName, object value, int row, int column)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (row < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (column < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
XSSFWorkbook workbook;
|
||||
using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
workbook = new XSSFWorkbook(fileStream);
|
||||
}
|
||||
|
||||
ISheet sheet = workbook.NumberOfSheets > 0 ? workbook.GetSheetAt(0) : workbook.CreateSheet();
|
||||
|
||||
IRow dataRow = sheet.GetRow(row - 1) ?? sheet.CreateRow(row - 1);
|
||||
ICell cell = dataRow.GetCell(column - 1) ?? dataRow.CreateCell(column - 1);
|
||||
|
||||
if (value is string strValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(strValue))
|
||||
{
|
||||
cell.SetCellValue(string.Empty);
|
||||
}
|
||||
else if (double.TryParse(strValue, out double numericValue))
|
||||
{
|
||||
cell.SetCellValue(numericValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.SetCellValue(strValue);
|
||||
}
|
||||
}
|
||||
else if (value is long longValue)
|
||||
{
|
||||
cell.SetCellValue((double)longValue);
|
||||
}
|
||||
else if (value is decimal decimalValue)
|
||||
{
|
||||
cell.SetCellValue((double)decimalValue);
|
||||
}
|
||||
else if (value is double doubleValue)
|
||||
{
|
||||
cell.SetCellValue(doubleValue);
|
||||
}
|
||||
|
||||
using (var fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
workbook.Write(fileStream, false);
|
||||
|
||||
}
|
||||
workbook.Close();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool RefreshFormulas(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
XSSFWorkbook workbook;
|
||||
using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
workbook = new XSSFWorkbook(fileStream);
|
||||
}
|
||||
|
||||
for (int i = 0; i < workbook.NumberOfSheets; i++)
|
||||
{
|
||||
ISheet sheet = workbook.GetSheetAt(i);
|
||||
sheet.ForceFormulaRecalculation = true;
|
||||
}
|
||||
|
||||
XSSFFormulaEvaluator.EvaluateAllFormulaCells(workbook);
|
||||
|
||||
using (var fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
workbook.Write(fileStream, false);
|
||||
}
|
||||
workbook.Close();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
excel_pajak/appsettings.json
Normal file
20
excel_pajak/appsettings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Port=55432;Database=andal_kharisma;User Id=postgres;Password=Release@2024;Timeout=60;CommandTimeout=120;ServerCompatibilityMode=NoTypeLoading;"
|
||||
},
|
||||
"SchemaList": [
|
||||
"_fpjs5fybbsar9cxyttn9ui5ndw"
|
||||
],
|
||||
"Year": "2025",
|
||||
"ExcelSettings": {
|
||||
"TemplatePath": "simulasi_perhitungan_ak.xlsx",
|
||||
"OutputFolder": "Output"
|
||||
}
|
||||
}
|
||||
29
excel_pajak/excel_pajak.csproj
Normal file
29
excel_pajak/excel_pajak.csproj
Normal file
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||
<PackageReference Include="NPOI" Version="2.7.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="simulasi_perhitungan_ak.xlsx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
BIN
excel_pajak/simulasi_perhitungan_ak.xlsx
Normal file
BIN
excel_pajak/simulasi_perhitungan_ak.xlsx
Normal file
Binary file not shown.
147
excel_pajak_test/DatabaseServiceIntegrationTests.cs
Normal file
147
excel_pajak_test/DatabaseServiceIntegrationTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using excel_pajak.Services;
|
||||
using Shouldly;
|
||||
using System;
|
||||
|
||||
namespace excel_pajak_test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DatabaseServiceIntegrationTests
|
||||
{
|
||||
private static string? _connectionString;
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassInitialize(TestContext context)
|
||||
{
|
||||
// Load configuration from appsettings.json
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false);
|
||||
|
||||
var configuration = builder.Build();
|
||||
|
||||
_connectionString = configuration["ConnectionStrings:DefaultConnection"];
|
||||
if (string.IsNullOrWhiteSpace(_connectionString))
|
||||
{
|
||||
throw new InvalidOperationException("Connection string is missing in test appsettings.json");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_ValidQuery_ReturnsResult()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 42", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("42");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryReturnsNull_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT NULL::text", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_EmptyResult_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 1 WHERE 1=0", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryReturnsString_ReturnsString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 'test value'", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("test value");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryReturnsInteger_ReturnsString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 12345", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("12345");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryReturnsTimestamp_ReturnsString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT NOW()", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldBeOfType<string>();
|
||||
Console.WriteLine($"Database timestamp: {result}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryVersion_ReturnsVersionString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT version()", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
result.ShouldStartWith("PostgreSQL");
|
||||
Console.WriteLine($"PostgreSQL version: {result}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_InvalidSQL_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<InvalidOperationException>(() => DatabaseService.ExecuteScalar("INVALID SQL SYNTAX", _connectionString!));
|
||||
|
||||
exception.Message.ShouldMatch(".*(PostgreSQL error|Database error).*");
|
||||
Console.WriteLine($"Expected error caught: {exception.Message}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryToNonExistentTable_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<InvalidOperationException>(() => DatabaseService.ExecuteScalar("SELECT * FROM nonexistent_table_xyz LIMIT 1", _connectionString!));
|
||||
|
||||
exception.Message.ShouldMatch(".*(Database error|PostgreSQL error).*");
|
||||
exception.InnerException.ShouldNotBeNull();
|
||||
Console.WriteLine($"Expected error caught: {exception.Message}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryCurrentDatabase_ReturnsDatabaseName()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT current_database()", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
Console.WriteLine($"Current database: {result}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Integration_QueryCurrentUser_ReturnsUserName()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT current_user", _connectionString!);
|
||||
|
||||
// Assert
|
||||
result.ShouldNotBeNull();
|
||||
Console.WriteLine($"Current user: {result}");
|
||||
}
|
||||
}
|
||||
147
excel_pajak_test/DatabaseServiceTests.cs
Normal file
147
excel_pajak_test/DatabaseServiceTests.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using excel_pajak.Services;
|
||||
using Shouldly;
|
||||
using System;
|
||||
|
||||
namespace excel_pajak_test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class DatabaseServiceTests
|
||||
{
|
||||
private const string ValidConnectionString = "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;";
|
||||
|
||||
#region Connection String Validation Tests
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_NullConnectionString_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => DatabaseService.ExecuteScalar("SELECT 1", null!));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_EmptyConnectionString_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => DatabaseService.ExecuteScalar("SELECT 1", string.Empty));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Validation Tests
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_NullQuery_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => DatabaseService.ExecuteScalar(null!, ValidConnectionString));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_EmptyQuery_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => DatabaseService.ExecuteScalar(string.Empty, ValidConnectionString));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_WhitespaceQuery_ThrowsArgumentException()
|
||||
{
|
||||
// Act & Assert
|
||||
Should.Throw<ArgumentException>(() => DatabaseService.ExecuteScalar(" ", ValidConnectionString));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Database Connection Tests
|
||||
|
||||
[TestMethod]
|
||||
public void ExecuteScalar_InvalidConnection_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
var invalidConnection = "Server=invalidhost;Port=9999;Database=nonexistent;User Id=invalid;Password=invalid;";
|
||||
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<InvalidOperationException>(() => DatabaseService.ExecuteScalar("SELECT 1", invalidConnection));
|
||||
|
||||
exception.Message.ShouldContain("Database error");
|
||||
exception.InnerException.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Execution Tests (Integration - requires PostgreSQL)
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_ValidQuery_ReturnsResult()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 42", ValidConnectionString);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("42");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_QueryReturnsNull_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT NULL::text", ValidConnectionString);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_EmptyResult_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 1 WHERE 1=0", ValidConnectionString);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_QueryReturnsString_ReturnsString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 'test value'", ValidConnectionString);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("test value");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_QueryReturnsInteger_ReturnsString()
|
||||
{
|
||||
// Act
|
||||
var result = DatabaseService.ExecuteScalar("SELECT 12345", ValidConnectionString);
|
||||
|
||||
// Assert
|
||||
result.ShouldBe("12345");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error Handling Tests
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires database connection
|
||||
public void ExecuteScalar_InvalidSQL_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Should.Throw<InvalidOperationException>(() => DatabaseService.ExecuteScalar("INVALID SQL SYNTAX", ValidConnectionString));
|
||||
|
||||
exception.Message.ShouldMatch(".*(PostgreSQL error|Database error).*");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
372
excel_pajak_test/ExcelServiceTests.cs
Normal file
372
excel_pajak_test/ExcelServiceTests.cs
Normal file
@@ -0,0 +1,372 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using excel_pajak.Services;
|
||||
using Shouldly;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace excel_pajak_test;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ExcelServiceTests
|
||||
{
|
||||
#region WriteExcel Parameter Validation Tests
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_EmptyFileName_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("", "value", 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_NullFileName_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel(null!, "value", 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_NonExistentFile_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("nonexistent.xlsx", "value", 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_InvalidRowZero_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("test.xlsx", "value", 0, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_InvalidRowNegative_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("test.xlsx", "value", -1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_InvalidColumnZero_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("test.xlsx", "value", 1, 0);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteExcel_InvalidColumnNegative_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.WriteExcel("test.xlsx", "value", 1, -1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InitExcel Parameter Validation Tests
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_EmptyOutputName_ReturnsFailure()
|
||||
{
|
||||
// Act - This test validates the empty string check happens before initialization check
|
||||
// by using a file that doesn't exist (which would fail if initialization check was first)
|
||||
var result = ExcelService.InitExcel("", "template.xlsx", "output");
|
||||
|
||||
// Assert - Should fail due to empty output name
|
||||
result.success.ShouldBeFalse();
|
||||
result.error.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_NullOutputName_ReturnsFailure()
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.InitExcel(null!, null!, null!);
|
||||
|
||||
// Assert
|
||||
result.success.ShouldBeFalse();
|
||||
result.error.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InitExcel Overwrite Control Tests
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_OverwriteFalse_ExistingFile_ThrowsIOException()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "excel_pajak_test_" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var templatePath = Path.Combine(tempDir, "template.xlsx");
|
||||
var outputPath = Path.Combine(tempDir, "output.xlsx");
|
||||
|
||||
// Create a minimal template file
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test");
|
||||
using (var fs = new FileStream(templatePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
// Create existing output file
|
||||
File.WriteAllText(outputPath, "existing content");
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.InitExcel("output", templatePath, tempDir, overwrite: false);
|
||||
|
||||
// Assert
|
||||
result.success.ShouldBeFalse();
|
||||
result.error.ShouldBeOfType<IOException>();
|
||||
result.error!.Message.ShouldContain("already exists");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_OverwriteTrue_ExistingFile_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "excel_pajak_test_" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var templatePath = Path.Combine(tempDir, "template.xlsx");
|
||||
var outputPath = Path.Combine(tempDir, "output.xlsx");
|
||||
|
||||
// Create a minimal template file
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test");
|
||||
using (var fs = new FileStream(templatePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
// Create existing output file
|
||||
File.WriteAllText(outputPath, "existing content");
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.InitExcel("output.xlsx", templatePath, tempDir, overwrite: true);
|
||||
|
||||
// Assert
|
||||
result.success.ShouldBeTrue();
|
||||
result.result.ShouldNotBeNull();
|
||||
File.Exists(result.result!).ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_OverwriteFalse_NewFile_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "excel_pajak_test_" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var templatePath = Path.Combine(tempDir, "template.xlsx");
|
||||
|
||||
// Create a minimal template file
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test");
|
||||
using (var fs = new FileStream(templatePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
var result = ExcelService.InitExcel("output.xlsx", templatePath, tempDir, overwrite: false);
|
||||
|
||||
// Assert
|
||||
result.success.ShouldBeTrue();
|
||||
result.result.ShouldNotBeNull();
|
||||
File.Exists(result.result!).ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InitExcel_DefaultOverwrite_ExistingFile_Overwrites()
|
||||
{
|
||||
// Arrange
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "excel_pajak_test_" + Guid.NewGuid());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var templatePath = Path.Combine(tempDir, "template.xlsx");
|
||||
var outputPath = Path.Combine(tempDir, "output.xlsx");
|
||||
|
||||
// Create a minimal template file
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test");
|
||||
using (var fs = new FileStream(templatePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
// Create existing output file
|
||||
File.WriteAllText(outputPath, "existing content");
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Default behavior should overwrite
|
||||
var result = ExcelService.InitExcel("output.xlsx", templatePath, tempDir);
|
||||
|
||||
// Assert
|
||||
result.success.ShouldBeTrue();
|
||||
result.result.ShouldNotBeNull();
|
||||
File.Exists(result.result!).ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Integration Tests (manual execution)
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // Requires proper initialization sequence - run manually
|
||||
public void IntegrationTest_InitializeAndInitExcel()
|
||||
{
|
||||
// This test requires manual setup - skip in CI
|
||||
Assert.Inconclusive("Requires manual configuration setup");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // File locking issues in parallel tests
|
||||
public void WriteExcel_WithValidFile_WritesValue_Integration()
|
||||
{
|
||||
// Arrange - Create a simple test Excel file using absolute path
|
||||
var testFilePath = Path.Combine(Path.GetTempPath(), "test_excel_integration.xlsx");
|
||||
|
||||
// Create a minimal workbook using NPOI
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
var sheet = workbook.CreateSheet("Test");
|
||||
var row = sheet.CreateRow(0);
|
||||
var cell = row.CreateCell(0);
|
||||
cell.SetCellValue((string?)"Initial");
|
||||
|
||||
using (var fs = new FileStream(testFilePath, FileMode.Create))
|
||||
{
|
||||
workbook.Write(fs, false);
|
||||
}
|
||||
workbook.Close();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - Write to cell
|
||||
var result = ExcelService.WriteExcel(testFilePath, "NewValue", 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up
|
||||
if (File.Exists(testFilePath))
|
||||
{
|
||||
try { File.Delete(testFilePath); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // File locking issues in parallel tests
|
||||
public void WriteExcel_LongOverload_WritesNumericValue()
|
||||
{
|
||||
// Arrange
|
||||
var testFilePath = Path.Combine(Path.GetTempPath(), "test_excel_long.xlsx");
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test").CreateRow(0).CreateCell(0).SetCellValue(0);
|
||||
using (var fs = new FileStream(testFilePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
try
|
||||
{
|
||||
// Act
|
||||
long testValue = 1234567890L;
|
||||
var result = ExcelService.WriteExcel(testFilePath, testValue, 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
|
||||
// Verify
|
||||
using (var fs = new FileStream(testFilePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
var wb = new NPOI.XSSF.UserModel.XSSFWorkbook(fs);
|
||||
var cellValue = wb.GetSheetAt(0).GetRow(0).GetCell(0).NumericCellValue;
|
||||
cellValue.ShouldBe(1234567890.0);
|
||||
wb.Close();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(testFilePath)) { try { File.Delete(testFilePath); } catch { } }
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Ignore] // File locking issues in parallel tests
|
||||
public void WriteExcel_LargeLongValue_MaintainsPrecision()
|
||||
{
|
||||
// Arrange
|
||||
var testFilePath = Path.Combine(Path.GetTempPath(), "test_excel_large_long.xlsx");
|
||||
var workbook = new NPOI.XSSF.UserModel.XSSFWorkbook();
|
||||
workbook.CreateSheet("Test").CreateRow(0).CreateCell(0).SetCellValue(0);
|
||||
using (var fs = new FileStream(testFilePath, FileMode.Create)) { workbook.Write(fs, false); }
|
||||
workbook.Close();
|
||||
|
||||
try
|
||||
{
|
||||
// Act - A large number that still fits within double's exact integer representation (53 bits)
|
||||
long testValue = 9007199254740991L; // 2^53 - 1
|
||||
var result = ExcelService.WriteExcel(testFilePath, testValue, 1, 1);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeTrue();
|
||||
|
||||
// Verify
|
||||
using (var fs = new FileStream(testFilePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
var wb = new NPOI.XSSF.UserModel.XSSFWorkbook(fs);
|
||||
var cellValue = wb.GetSheetAt(0).GetRow(0).GetCell(0).NumericCellValue;
|
||||
cellValue.ShouldBe((double)testValue);
|
||||
wb.Close();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(testFilePath)) { try { File.Delete(testFilePath); } catch { } }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
1
excel_pajak_test/MSTestSettings.cs
Normal file
1
excel_pajak_test/MSTestSettings.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
11
excel_pajak_test/Test1.cs
Normal file
11
excel_pajak_test/Test1.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace excel_pajak_test
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class Test1
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestMethod1()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
15
excel_pajak_test/appsettings.json
Normal file
15
excel_pajak_test/appsettings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Port=55432;Database=andal_kharisma;User Id=postgres;Password=Release@2024;Timeout=60;CommandTimeout=120;ServerCompatibilityMode=NoTypeLoading;"
|
||||
},
|
||||
|
||||
"SchemaList": ["_onx4pzkwkeortehfjthgyfkb7c"],
|
||||
"Year": "2025"
|
||||
}
|
||||
34
excel_pajak_test/excel_pajak_test.csproj
Normal file
34
excel_pajak_test/excel_pajak_test.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="MSTest.Sdk/4.0.1">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\excel_pajak\excel_pajak.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NPOI" Version="2.7.5" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseVSTest>true</UseVSTest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="MSTest.TestAdapter" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="MSTest.TestFramework" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
20
openspec/config.yaml
Normal file
20
openspec/config.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
schema: spec-driven
|
||||
|
||||
# Project context (optional)
|
||||
# This is shown to AI when creating artifacts.
|
||||
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||
# Example:
|
||||
# context: |
|
||||
# Tech stack: TypeScript, React, Node.js
|
||||
# We use conventional commits
|
||||
# Domain: e-commerce platform
|
||||
|
||||
# Per-artifact rules (optional)
|
||||
# Add custom rules for specific artifacts.
|
||||
# Example:
|
||||
# rules:
|
||||
# proposal:
|
||||
# - Keep proposals under 500 words
|
||||
# - Always include a "Non-goals" section
|
||||
# tasks:
|
||||
# - Break tasks into chunks of max 2 hours
|
||||
10
openspec/specs/excel-service-write-long/spec.md
Normal file
10
openspec/specs/excel-service-write-long/spec.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: ExcelService shall support writing long values
|
||||
The `ExcelService` class SHALL provide a `WriteExcel` overload that accepts a `long` value.
|
||||
|
||||
#### Scenario: WriteExcel with valid long value
|
||||
- **WHEN** `WriteExcel(string fileName, long value, int row, int column)` is called with valid parameters
|
||||
- **THEN** the system SHALL write the numeric `long` value to the specified cell
|
||||
- **AND** the cell type in Excel SHALL be numeric
|
||||
- **AND** the file SHALL be saved successfully
|
||||
39
openspec/specs/file-overwrite-control/spec.md
Normal file
39
openspec/specs/file-overwrite-control/spec.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# file-overwrite-control Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-file-overwrite-control. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: ExcelService provides file overwrite control
|
||||
The ExcelService SHALL provide explicit control over whether existing files should be overwritten during initialization.
|
||||
|
||||
#### Scenario: Create new file when file does not exist
|
||||
- **WHEN** `InitExcel` is called with `overwrite=true` and file does not exist
|
||||
- **THEN** method creates new file and returns success with file path
|
||||
|
||||
#### Scenario: Overwrite existing file when overwrite is true
|
||||
- **WHEN** `InitExcel` is called with `overwrite=true` and file exists
|
||||
- **THEN** method overwrites existing file and returns success with file path
|
||||
|
||||
#### Scenario: Throw exception when file exists and overwrite is false
|
||||
- **WHEN** `InitExcel` is called with `overwrite=false` and file exists
|
||||
- **THEN** method throws `IOException` with message indicating file already exists
|
||||
|
||||
#### Scenario: Create file when overwrite is false and file does not exist
|
||||
- **WHEN** `InitExcel` is called with `overwrite=false` and file does not exist
|
||||
- **THEN** method creates new file and returns success with file path
|
||||
|
||||
#### Scenario: Preserve existing file behavior when overwrite parameter is not provided
|
||||
- **WHEN** `InitExcel` is called without the `overwrite` parameter
|
||||
- **THEN** method behaves as current implementation (overwrites file)
|
||||
|
||||
### Requirement: ExcelService maintains backward compatibility
|
||||
The ExcelService SHALL maintain existing behavior for all current method signatures and parameters.
|
||||
|
||||
#### Scenario: Existing code continues to work without changes
|
||||
- **WHEN** existing code calls `InitExcel` without the `overwrite` parameter
|
||||
- **THEN** method behaves identically to current implementation (overwrites file)
|
||||
|
||||
#### Scenario: Existing error handling patterns remain valid
|
||||
- **WHEN** existing code checks the result tuple for success/failure
|
||||
- **THEN** method returns same tuple structure and error types as current implementation
|
||||
|
||||
56
openspec/specs/formula-refresh/spec.md
Normal file
56
openspec/specs/formula-refresh/spec.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Capability: Formula Refresh
|
||||
|
||||
## Purpose
|
||||
The Formula Refresh capability allows the system to recalculate all formulas in an Excel workbook and update their cached values. This is essential when data has been written to the workbook externally and the calculated results need to be persisted within the file itself.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Refresh all formulas in workbook
|
||||
The ExcelService SHALL provide a public method `RefreshFormulas` that evaluates and recalculates all formulas in an Excel workbook.
|
||||
|
||||
#### Scenario: Successful formula refresh
|
||||
- **WHEN** a valid Excel file containing formulas is provided to `RefreshFormulas`
|
||||
- **THEN** all formulas in the workbook are evaluated
|
||||
- **AND** cached formula values are updated with recalculated results
|
||||
- **AND** the workbook is saved back to the file
|
||||
- **AND** the method returns `(success: true, result: <filePath>, error: null)`
|
||||
|
||||
#### Scenario: File not found
|
||||
- **WHEN** `RefreshFormulas` is called with a file path that does not exist
|
||||
- **THEN** the method returns `(success: false, result: null, error: FileNotFoundException)`
|
||||
|
||||
#### Scenario: Invalid or corrupted workbook
|
||||
- **WHEN** `RefreshFormulas` is called with a file that is not a valid Excel workbook
|
||||
- **THEN** the method returns `(success: false, result: null, error: <exception>)`
|
||||
- **AND** the original file remains unmodified
|
||||
|
||||
#### Scenario: Empty file path
|
||||
- **WHEN** `RefreshFormulas` is called with null or empty file path
|
||||
- **THEN** the method returns `(success: false, result: null, error: ArgumentException)`
|
||||
|
||||
### Requirement: Formula evaluation completeness
|
||||
The `RefreshFormulas` method SHALL evaluate all formula types across all sheets in the workbook.
|
||||
|
||||
#### Scenario: Multiple sheets with formulas
|
||||
- **WHEN** a workbook contains multiple sheets with formulas
|
||||
- **THEN** formulas in all sheets are evaluated
|
||||
- **AND** cross-sheet references are properly resolved
|
||||
|
||||
#### Scenario: Different formula types
|
||||
- **WHEN** a workbook contains various formula types (SUM, AVERAGE, IF, VLOOKUP, etc.)
|
||||
- **THEN** all formula types are evaluated correctly
|
||||
- **AND** formulas dependent on other formulas are evaluated in correct order
|
||||
|
||||
### Requirement: Resource management
|
||||
The `RefreshFormulas` method SHALL properly dispose of all resources including file streams and workbook objects.
|
||||
|
||||
#### Scenario: Proper disposal after successful refresh
|
||||
- **WHEN** formula refresh completes successfully
|
||||
- **THEN** all file streams are closed and disposed
|
||||
- **AND** the workbook object is disposed
|
||||
- **AND** no file locks remain on the Excel file
|
||||
|
||||
#### Scenario: Proper disposal after error
|
||||
- **WHEN** an exception occurs during formula refresh
|
||||
- **THEN** all allocated resources are still disposed
|
||||
- **AND** no file locks remain on the Excel file
|
||||
89
openspec/specs/postgresql-query/spec.md
Normal file
89
openspec/specs/postgresql-query/spec.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Configuration loading from appsettings.json
|
||||
The system SHALL load the PostgreSQL connection string from `appsettings.json` using the `ConnectionStrings:DefaultConnection` configuration key.
|
||||
|
||||
#### Scenario: Successful configuration loading
|
||||
- **WHEN** the application starts and `appsettings.json` exists with a valid `ConnectionStrings:DefaultConnection` entry
|
||||
- **THEN** the system shall successfully load the connection string
|
||||
- **AND** the connection string shall be available for database connections
|
||||
|
||||
#### Scenario: Missing connection string
|
||||
- **WHEN** the application starts and `appsettings.json` is missing the `ConnectionStrings:DefaultConnection` entry
|
||||
- **THEN** the system shall throw an `InvalidOperationException` with a clear error message indicating the missing configuration
|
||||
|
||||
#### Scenario: Invalid appsettings.json format
|
||||
- **WHEN** the application starts and `appsettings.json` contains invalid JSON
|
||||
- **THEN** the system shall throw a configuration exception indicating the JSON parsing error
|
||||
|
||||
### Requirement: Database query execution
|
||||
The system SHALL provide a function that accepts a SQL query string and returns the first column of the first row as a string value.
|
||||
|
||||
#### Scenario: Successful query execution with single result
|
||||
- **WHEN** a valid SQL query is executed that returns a single row with a single column
|
||||
- **THEN** the system shall return the value as a string
|
||||
- **AND** the database connection shall be properly opened and closed
|
||||
|
||||
#### Scenario: Query returning NULL value
|
||||
- **WHEN** a SQL query is executed that returns a NULL value in the first column of the first row
|
||||
- **THEN** the system shall return `null` instead of throwing an exception
|
||||
|
||||
#### Scenario: Query returning no rows
|
||||
- **WHEN** a SQL query is executed that returns zero rows
|
||||
- **THEN** the system shall return `null`
|
||||
|
||||
#### Scenario: Invalid SQL syntax
|
||||
- **WHEN** a SQL query with invalid syntax is executed
|
||||
- **THEN** the system shall throw a `PostgresException` with the PostgreSQL error details
|
||||
|
||||
#### Scenario: Database connection failure
|
||||
- **WHEN** the database server is unreachable or credentials are invalid
|
||||
- **THEN** the system shall throw a `PostgresException` or `NpgsqlException` with connection error details
|
||||
|
||||
### Requirement: Connection management
|
||||
The system SHALL properly manage database connections using proper disposal patterns to prevent connection leaks.
|
||||
|
||||
#### Scenario: Connection properly disposed after query
|
||||
- **WHEN** a query is executed successfully
|
||||
- **THEN** the database connection shall be disposed after the query completes
|
||||
- **AND** the connection shall be returned to the connection pool
|
||||
|
||||
#### Scenario: Connection disposed on exception
|
||||
- **WHEN** a query execution throws an exception
|
||||
- **THEN** the database connection shall still be properly disposed
|
||||
- **AND** no connection leak shall occur
|
||||
|
||||
### Requirement: Static Service Initialization
|
||||
The system SHALL provide a static method to initialize the database service with a connection string.
|
||||
|
||||
#### Scenario: Successful static initialization
|
||||
- **WHEN** the `Initialize` method is called with a valid connection string
|
||||
- **THEN** the static internal state shall be updated
|
||||
- **AND** subsequent query calls shall use this connection string
|
||||
|
||||
### Requirement: Synchronous Database Query Execution
|
||||
The system SHALL provide static functions that accept SQL query strings and return results synchronously.
|
||||
|
||||
#### Scenario: Successful synchronous query execution
|
||||
- **WHEN** a valid SQL query is executed via a static method
|
||||
- **THEN** the system shall return the result string synchronously
|
||||
- **AND** the database connection shall be properly opened and closed synchronously
|
||||
|
||||
### Requirement: Static Configuration Access
|
||||
The system SHALL provide static access to configuration properties through a dedicated service.
|
||||
|
||||
#### Scenario: Accessing connection string via static method
|
||||
- **WHEN** the static `GetConnectionString()` method is called
|
||||
- **THEN** it shall return the connection string from the internal static configuration
|
||||
- **AND** it shall throw an `InvalidOperationException` if the configuration is missing the required key
|
||||
|
||||
### Requirement: Static Configuration Initialization
|
||||
The system SHALL provide a way to initialize the static configuration state.
|
||||
|
||||
#### Scenario: Default static initialization
|
||||
- **WHEN** the configuration service is first accessed without explicit initialization
|
||||
- **THEN** it shall automatically load configuration from `appsettings.json`
|
||||
|
||||
#### Scenario: Explicit static initialization
|
||||
- **WHEN** the `Initialize` method is called with a custom `IConfiguration` object
|
||||
- **THEN** the static internal state shall be updated with the provided configuration
|
||||
176
query_employee_data.sql
Normal file
176
query_employee_data.sql
Normal file
@@ -0,0 +1,176 @@
|
||||
with months as (
|
||||
select '202501' as payment_period
|
||||
union all select '202502'
|
||||
union all select '202503'
|
||||
union all select '202504'
|
||||
union all select '202505'
|
||||
union all select '202506'
|
||||
union all select '202507'
|
||||
union all select '202508'
|
||||
union all select '202509'
|
||||
union all select '202510'
|
||||
union all select '202511'
|
||||
union all select '202512'
|
||||
),
|
||||
tax_calculation_basal as (
|
||||
select erp."TaxCalculationType"
|
||||
from _onx4pzkwkeortehfjthgyfkb7c."EmployeeReportPeriodics" erp
|
||||
inner join _onx4pzkwkeortehfjthgyfkb7c."Employees" e on erp."EmployeeId" = e."Id"
|
||||
where erp."PaymentPeriod" between '202501' and '202512'
|
||||
and e."NIK" = 'PS2MA001'
|
||||
and erp."BasicSalary" > 0
|
||||
order by erp."PaymentPeriod" desc
|
||||
limit 1
|
||||
),
|
||||
matched_records as (
|
||||
select e."NIK" as employee_number,
|
||||
e."Name" as employee_name,
|
||||
e."NPWP" as employee_npwp,
|
||||
(case
|
||||
when erp."TaxStatus"=1 then 'TK0'
|
||||
when erp."TaxStatus"=2 then 'TK1'
|
||||
when erp."TaxStatus"=3 then 'TK2'
|
||||
when erp."TaxStatus"=4 then 'TK3'
|
||||
when erp."TaxStatus"=5 then 'K0'
|
||||
when erp."TaxStatus"=6 then 'K1'
|
||||
when erp."TaxStatus"=7 then 'K2'
|
||||
when erp."TaxStatus"=8 then 'K3'
|
||||
end) as tax_marital_status,
|
||||
ets."EmployeeTaxStatusCode" as employee_tax_status,
|
||||
m.payment_period,
|
||||
erp."NPWPPemotong" as npwp_pemotong,
|
||||
(case erp."TaxCalculationType" when 2 then 'PaidByEmployee' when 3 then 'PaidAsAllowance' else 'PaidByCompany' end) as tax_type,
|
||||
erp."BasicSalary" as basic_salary,
|
||||
erp."Overtime" as overtime,
|
||||
erp."AllowanceRegular" as allowance,
|
||||
erp."Deductions" as deduction,
|
||||
erp."Natura" as benefit_in_kind,
|
||||
erp."Severance" as severance,
|
||||
erp."JKKCompany" as jkk,
|
||||
erp."JKMCompany" as jkm,
|
||||
erp."BPJSKesCompany" + erp."OtherInsuranceCompany" as bpjs_kesehatan_company,
|
||||
erp."JHTEmployee" as jht_emp,
|
||||
erp."JPEmployee" as jp_emp,
|
||||
0 as pension_emp,
|
||||
erp."EmployeeCondition" as employee_condition,
|
||||
erp."ResignType" as resign_type,
|
||||
erp."AllowanceTaxRegular" as allowance_tax_regular,
|
||||
erp."TaxRegular" as tax_regular,
|
||||
erp."AllowanceIrregular" as allowance_irregular,
|
||||
0 as benefit_in_kind_irregular,
|
||||
erp."AllowanceTaxIrregular" as allowance_tax_irregular,
|
||||
erp."TaxIrregular" as tax_irregular,
|
||||
0 as severance_tax,
|
||||
0 as allowance_severance_tax,
|
||||
erp."BegSalaryNetto" as beginning_salary_netto,
|
||||
erp."BegPPh21" as beginning_pph21
|
||||
from months m
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."EmployeeReportPeriodics" erp
|
||||
on m.payment_period = erp."PaymentPeriod"
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."Employees" e
|
||||
on erp."EmployeeId" = e."Id"
|
||||
and e."NIK" = 'PS2MA001'
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."EmployeeTaxStatuses" ets
|
||||
on e."EmployeeTaxStatusRefId" = ets."Id"
|
||||
where erp."TaxCalculationType" = (select "TaxCalculationType" from tax_calculation_basal)
|
||||
),
|
||||
unmatched_records as (
|
||||
select e."NIK" as employee_number,
|
||||
e."Name" as employee_name,
|
||||
e."NPWP" as employee_npwp,
|
||||
(case
|
||||
when erp."TaxStatus"=1 then 'TK0'
|
||||
when erp."TaxStatus"=2 then 'TK1'
|
||||
when erp."TaxStatus"=3 then 'TK2'
|
||||
when erp."TaxStatus"=4 then 'TK3'
|
||||
when erp."TaxStatus"=5 then 'K0'
|
||||
when erp."TaxStatus"=6 then 'K1'
|
||||
when erp."TaxStatus"=7 then 'K2'
|
||||
when erp."TaxStatus"=8 then 'K3'
|
||||
end) as tax_marital_status,
|
||||
ets."EmployeeTaxStatusCode" as employee_tax_status,
|
||||
m.payment_period,
|
||||
erp."NPWPPemotong" as npwp_pemotong,
|
||||
(case erp."TaxCalculationType" when 2 then 'PaidByEmployee' when 3 then 'PaidAsAllowance' else 'PaidByCompany' end) as tax_type,
|
||||
erp."BasicSalary" as basic_salary,
|
||||
erp."Overtime" as overtime,
|
||||
erp."AllowanceRegular" as allowance,
|
||||
erp."Deductions" as deduction,
|
||||
erp."Natura" as benefit_in_kind,
|
||||
erp."Severance" as severance,
|
||||
erp."JKKCompany" as jkk,
|
||||
erp."JKMCompany" as jkm,
|
||||
erp."BPJSKesCompany" + erp."OtherInsuranceCompany" as bpjs_kesehatan_company,
|
||||
erp."JHTEmployee" as jht_emp,
|
||||
erp."JPEmployee" as jp_emp,
|
||||
0 as pension_emp,
|
||||
erp."EmployeeCondition" as employee_condition,
|
||||
erp."ResignType" as resign_type,
|
||||
erp."AllowanceTaxRegular" as allowance_tax_regular,
|
||||
erp."TaxRegular" as tax_regular,
|
||||
erp."AllowanceIrregular" as allowance_irregular,
|
||||
0 as benefit_in_kind_irregular,
|
||||
erp."AllowanceTaxIrregular" as allowance_tax_irregular,
|
||||
erp."TaxIrregular" as tax_irregular,
|
||||
0 as severance_tax,
|
||||
0 as allowance_severance_tax,
|
||||
erp."BegSalaryNetto" as beginning_salary_netto,
|
||||
erp."BegPPh21" as beginning_pph21
|
||||
from months m
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."EmployeeReportPeriodics" erp
|
||||
on m.payment_period = erp."PaymentPeriod"
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."Employees" e
|
||||
on erp."EmployeeId" = e."Id"
|
||||
and e."NIK" = 'PS2MA001'
|
||||
left join _onx4pzkwkeortehfjthgyfkb7c."EmployeeTaxStatuses" ets
|
||||
on e."EmployeeTaxStatusRefId" = ets."Id"
|
||||
where erp."TaxCalculationType" <> (select "TaxCalculationType" from tax_calculation_basal)
|
||||
and erp."TaxCalculationType" = 0
|
||||
),
|
||||
empty_records as (
|
||||
select null::text as employee_number,
|
||||
''::text as employee_name,
|
||||
''::text as employee_npwp,
|
||||
null::text as tax_marital_status,
|
||||
null::integer as employee_tax_status,
|
||||
m.payment_period,
|
||||
''::text as npwp_pemotong,
|
||||
''::text as tax_type,
|
||||
0::numeric as basic_salary,
|
||||
0::numeric as overtime,
|
||||
0::numeric as allowance,
|
||||
0::numeric as deduction,
|
||||
0::numeric as benefit_in_kind,
|
||||
0::numeric as severance,
|
||||
0::numeric as jkk,
|
||||
0::numeric as jkm,
|
||||
0::numeric as bpjs_kesehatan_company,
|
||||
0::numeric as jht_emp,
|
||||
0::numeric as jp_emp,
|
||||
0::numeric as pension_emp,
|
||||
null::integer as employee_condition,
|
||||
null::integer as resign_type,
|
||||
0::numeric as allowance_tax_regular,
|
||||
0::numeric as tax_regular,
|
||||
0::numeric as allowance_irregular,
|
||||
0::numeric as benefit_in_kind_irregular,
|
||||
0::numeric as allowance_tax_irregular,
|
||||
0::numeric as tax_irregular,
|
||||
0::numeric as severance_tax,
|
||||
0::numeric as allowance_severance_tax,
|
||||
0::numeric as beginning_salary_netto,
|
||||
0::numeric as beginning_pph21
|
||||
from months m
|
||||
where not exists (
|
||||
select 1 from matched_records mr where mr.payment_period = m.payment_period
|
||||
)
|
||||
and not exists (
|
||||
select 1 from unmatched_records ur where ur.payment_period = m.payment_period
|
||||
)
|
||||
)
|
||||
select * from matched_records
|
||||
union all
|
||||
select * from unmatched_records
|
||||
union all
|
||||
select * from empty_records
|
||||
order by payment_period
|
||||
Reference in New Issue
Block a user