Generator: pilot analysis and fixes; G0002-G0021 tasks; handover notes merged

- Static bundle checks (name length, seed type, reserved words, contract test classes,
  testclasses_file), local abaplint parser check before SAP, max_tokens, robust JSON parse
- Runner: G2 for CDS views with parameters, contract_check detail in report
- G0020 and G0013 fixed by review and revalidated (oracle 100, null 0)
- Pilot results in docs/faz1-tasarim.md 11e; handover notes merged into CLAUDE.md and docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-03 05:04:23 +02:00
parent 15eb9f1bb2
commit 5de5e2851c
162 changed files with 9943 additions and 23 deletions

View File

@@ -1,8 +1,15 @@
# ABAP LLM harness — instructions for Claude Code
Talk to the user (Kral) in Turkish. Keep answers short; remove words that add no value.
When you write English text (specs, prompts, docs for the model), use ASD-STE100 Simplified Technical English.
Ask before an action that uses much cloud budget.
## 0. Working with Kral
- Talk in Turkish. Short answers. Remove words and sentences that add no value.
- English text (specs, prompts, docs for the model): ASD-STE100 Simplified Technical English.
- Ask before an action that costs much (cloud budget) or that changes the A4H system outside harness runs.
- For a series of updates to one document, deliver the result directly as a Markdown file; do not ask each time.
- Record each decision with its date in `docs/yol-haritasi.md` or `docs/faz1-tasarim.md`.
- Commit the current changes before a new generation or test batch.
- Kral has no time for a full review. Eval review = automatic gates + empirical filter (2–3 models) +
Claude review with a checklist; Kral reviews only flagged tasks + one per category (~10 tasks).
## 1. Project
@@ -12,6 +19,12 @@ Ask before an action that uses much cloud budget.
- Contract of the model: "ABAP task as text + generic ABAP MCP interface".
The plan format (plan-agent.md) gives the best result, but it is not mandatory.
- Training data: never Claude output. Eval tasks never go into training data.
- Input: the plan format gives the best result but is not mandatory. The old playbook format is not used.
- Generic ABAP MCP interface (core: search_object, read_source, object_structure, where_used, create_object,
write_source, activate, syntax_check, run_unit_tests, run_atc). Small tool name/schema variation in training.
- Category K: incomplete or free-text input + other tool schema.
- Object type mix: CLAS/INTF 35 %, FUNC 15 %, PROG 10 %, DDIC 10 %, CDS 25 %, MSAG + exception 5 %.
- Teacher and task author: DeepSeek V4.1 Flash (Ollama cloud). Budget calendar in `docs/yol-haritasi.md`.
- Read first: `docs/yol-haritasi.md` (roadmap, current position) and `docs/faz1-tasarim.md` (design).
When a decision or a result changes, update these files.
@@ -76,21 +89,27 @@ python3 -c "from harness.ledger import spent; print(spent())"
- Done: harness on Mac mini; test include creation in the server; teardown; FUNC/PROG/DDLS support;
task generator; ledger and budget guard.
- Results: T01 oracle 100, Qwen 27B 41.7 (old run 103), DeepSeek 98.5. T13 DeepSeek 85, T14 DeepSeek 100.
- Running: pilot (`harness.pilot`, 20 tasks) → `runs/gen/pilot.json`, log `runs/gen/pilot.log`.
A macOS notification shows when it ends.
- First generated task G0001: not accepted after 3 attempts (activation error, seed save error,
reference failed 1 hidden test). Feedback now includes the reference write/activation errors.
- Pilot (G0002–G0021) done: 16/20 accepted, 0.16 USD per accepted task (`runs/gen/pilot.json`,
analysis in `docs/faz1-tasarim.md` 11e). Generator improved after the pilot: static checks
(name length, seed type, reserved words, contract/test classes), local abaplint parser check before SAP,
max_tokens 100k, G2 detail in the repair feedback. Runner: G2 works for CDS views with parameters.
The improved generator is not yet tested with new tasks.
- G0020 (rejected) passes with oracle 100 after one fix (remove parameter `default`). G0013 (accepted) has a test
class in its contract: fix in review.
## 7. Next steps
1. Analyze the pilot: acceptance rate, failure causes, cost per task. Improve the generator prompt.
1. Test the improved generator with a small batch (~5 tasks, ~0.7 USD); compare repairs per task with the pilot (2.75 calls).
2. Step D: mutation check (hidden tests must fail on a broken reference).
3. Step F: eval set of 110 tasks: ~150 candidates, empirical filter (2–3 models), Claude review with a
checklist, Kral spot-checks ~10 flagged tasks.
4. Scoring for stop tasks (category H) and category K (free-text input, other tool schema).
5. Step 1.3b: generic ABAP MCP interface (spec `abap-mcp-arayuz.md`, later). The proxy is the first adapter.
## 8. Open items for Kral (server)
## 8. Open items for Kral (EPOD server)
- When a write fails, run a syntax check and return its messages. Now the server says only "save failed"
(for example for `TYPE c LENGTH n` in a method signature). The model cannot see the cause and cannot repair;
this hurts eval and training. Workaround in the generator: local abaplint parser check.
- Concurrency: queue calls per RFC connection, or use a connection pool.
- BDEF creation (needed for RAP tasks).

603
docs/faz1-tasarim.md Normal file
View File

@@ -0,0 +1,603 @@
# Faz 1 Tasarım — ABAP Danışman Modeli: Eval Seti, Görev Formatı, Harness
Durum: taslak v23 · 2026-10-02 · v23: Adım B — FM, rapor, CDS desteği; T13–T15; eşzamanlılık bulgusu.
## 1. Amaç
Faz 1 iki şey üretir:
1. **Eval seti:** A4H'da otomatik puanlanan ABAP görevleri. Hedef 100 görev; bu dokümanda 10 örnek.
2. **Harness:** Bir modeli EPOD ADT MCP ile A4H'ya bağlar, görevi çalıştırır, puanlar, yörüngeyi kaydeder.
Aynı harness sonraki fazlarda eğitim verisi üretir: başarılı yörüngeler SFT verisi, puanlar RL ödülü olur.
## 2. Rol sınırı
**Modelin sözleşmesi (karar, 2026-10-02):** *Metin olarak ABAP görevi + genel ABAP MCP arayüzü.* Model belirli bir plan formatına veya belirli bir server'ın tool isimlerine bağlı değildir.
`plan-agent.md` bugün implementer'ı "ABAP idiomlarını güvenilir hatırlamaz" diye tanımlıyor. Bu yüzden zanaat kararlarını planner veriyor. Yeni düzende bu kararlar modele geçer.
| Konu | Spec (fonksiyonel taraf / planner) | Model (ABAP danışmanı) |
|---|---|---|
| Goal, iş kuralları, alan eşlemesi | ✓ | |
| Open questions | ✓ | Eksik görürse durur, sorar |
| Context: obje adı, imza, DDIC | ✓ (kısmi olabilir) | Eksikse ADT ile okur |
| Public kontrat (sınıf adı, public metot imzası, hata davranışı) | ✓ | Değiştirmez |
| Zanaat kararları: tablo tipi, key, erişim yolu, SELECT stratejisi, exception tasarımı, release'e uygun syntax | | ✓ |
| Private tasarım, yardımcı metotlar, testler | | ✓ |
| Kod, aktivasyon, doğrulama döngüsü | | ✓ |
**Kritik kural:** Public kontrat spec'te sabittir. Gizli testler bu kontrata bağlanır. İç tasarım tamamen modelindir.
## 3. Spec formatı (modelin girdisi)
`plan-agent.md` yapısı temel alınır, dil STE100 kalır. Değişiklikler:
- **Decisions** bölümü kaldırılır. Sadece iş kararları kalır ("Business rules").
- **Steps** iş seviyesine çekilir. Obje sırası ve zanaat detayı modele bırakılır.
- **Contract** ayrı bölüm olur. Gizli testlerin bağlandığı yer burası.
- **Constraints** eklenir. İçeriği:
- Hedef release, paket, isim öneki.
- Coding standards: projeye özgü, denetlenebilir kurallar.
- Out of scope: dokunulmayacak objeler ve eklenmeyecek davranışlar.
Bölümler, bu sırayla:
1. Goal
2. Open questions (pozitif görevlerde boş)
3. Context
4. Contract
5. Business rules
6. Constraints
7. Acceptance
**Plan formatının rolü (karar, 2026-10-02):** `plan-agent.md` formatı zorunlu girdi değil, **en iyi sonuç veren** girdidir. Model serbest metin ve eksik görev tanımıyla da çalışabilmelidir. Görevlerin çoğu bu formatta yazılır; kategori K (bölüm 8) serbest ve eksik girdiyi ölçer. Kral'ın kendi akışında (`/sapplan` → build) plan kullanımı değişmez.
**Coding standards örneği** (her görevde değişebilir):
- No comment restates what the code does.
- No Given/When/Then comment header in a test.
- Method length stays below 40 statements.
- No new global variable.
- No method chaining (release kısıtı olan görevlerde).
- Use text elements for user-facing text.
Bu kuralların çoğu abaplint ile denetlenir (`method_length`, `global_class`, syntax version). Kalanlar özel AST kontrolüdür.
## 4. Model çıktısı
Model şunları üretir:
1. **Decisions bloğu** (STE100): `plan-agent.md` Decisions listesindeki maddeler. Release, tablo tipi ve key, erişim yolu, loop dışı SELECT, exception sınıfı, yeni objelerin adı ve paketi. Her biri tek satır gerekçeyle.
2. **Objeler:** A4H'da oluşturulmuş ve aktif.
3. **Kendi testleri:** ABAP Unit, modelin kendi yazdığı.
4. **ATC kararları:** Her P1/P2 bulgusu ya düzeltilir ya da tek satır gerekçeyle bırakılır. False positive belgelenir.
5. **Rapor** (STE100): `build-agent.md` Reporting kuralı. Her adım, doğrulama sonucu, sapmalar.
6. **Change summary tablosu:** Object · Type · Action · Reason. Throwaway objeler (`$TMP` smoke sınıfı vb.) ayrı işaretlenir.
Davranış kuralları `build-agent.md`'den uyarlanır:
- Public kontratı değiştirme. Değiştirmen gerekiyorsa dur ve raporla.
- İş kuralı eksik veya çelişkiliyse dur ve sor. Tahmin etme.
- Zanaat kararı spec'te yoksa **kendin ver**. (Eski kural: "dur ve sor". Bu kural tersine döner.)
- Spec'te istenmeyen davranış ekleme.
- Constraints altındaki out of scope listesine dokunma.
- Aynı doğrulama iki kez başarısız olursa dur ve raporla. Üçüncü deneme yok.
- ABAP Unit'in kapsayamadığı bir davranışı (ör. factory'nin runtime çözümü) `$TMP` smoke objesiyle doğrula. Smoke objesi teslimata girmez.
- Testler gerçek veritabanını okumaz. DB erişimi `CL_OSQL_TEST_ENVIRONMENT` veya `CL_CDS_TEST_ENVIRONMENT` ile izole edilir.
## 5. Ortam
**Sistem:** A4H, Mac mini üzerinde Docker.
**Sistem kimliği:** MCP server'da tek sistem bağlı: `epodabap`, mod `write`. SAP_BASIS 816 SP01. Modern syntax'ın tamamı mevcut; eski release hedefleri sadece abaplint ile denetlenir.
**İzolasyon:** MCP server paket (DEVC) oluşturamaz. Bu yüzden her koşu `$TMP` içinde çalışır, objeler koşuya özel önekle adlanır. Önek şeması: `Z<run 3 hane><task 3 hane>_` (ör. `Z007003_DISC_CALC`). Önek her koşuda yenidir; eski koşunun objeleriyle çakışma olmaz. Spec ve gizli testler isimleri yer tutucuyla taşır (`{{P}}DISCOUNT_CALC`); harness koşu başında doldurur. 30 karakter sınırı görev yazımında kontrol edilir.
**Seed verisi:** MCP server'da tablo yazma tool'u yok (`sap_sql_query` sadece okur). Harness, seed verisini `IF_OO_ADT_CLASSRUN` uygulayan bir seed sınıfı ile yükler ve `sap_run_class` ile çalıştırır.
**Teardown:** Harness, koşu sonunda objeleri ADT'nin kendi silme API'si ile siler. MCP server değişmez, model bu yolu görmez.
```
POST /sap/bc/adt/deletion/delete
Accept: application/vnd.sap.adt.deletion.response.v1+xml
Content-Type: application/vnd.sap.adt.deletion.request.v1+xml
<del:deletionRequest xmlns:del="http://www.sap.com/adt/deletion"
xmlns:adtcore="http://www.sap.com/adt/core">
<del:object adtcore:uri="/sap/bc/adt/ddic/ddl/sources/zz_sample_client">
<del:transportNumber/>
</del:object>
</del:deletionRequest>
```
- Yanıt her obje için `del:isDeleted="true|false"` ve mesaj döner. DDIC objelerinde silme log linki gelir.
- Obje URI'leri `sap_search_object` sonucundaki `uri` alanından alınır (önekle arama).
- `$TMP` objelerinde `transportNumber` boş kalır.
- Harness, CSRF token'ı (`x-csrf-token: fetch`) ve oturum cookie'sini yönetir.
**Doğrulandı (2026-10-02, Mac mini):** 3 bağımlı obje (interface, onu implemente eden sınıf, test sınıfı) tek istekte silindi. Toplam 20 objeden 19'u silindi. Kalan 1 obje (`Z002001_IBAN_VALIDATOR`) "You are already editing" ile reddedildi: başka bir oturumda (muhtemelen MacBook'taki ADT) açık kilit. Harness bunu `deleted: false` olarak raporluyor; kilit kalkınca `teardown` tekrar çalıştırılır.
Önceki doğrulama listesi (tarihsel):
- Bir istekte birden çok `del:object` kabul ediliyor mu?
- Bağımlı objeler (ör. sınıf + kullandığı tablo tipi) aynı istekte doğru sırayla siliniyor mu? Değilse harness sırayı verir: test sınıfı → sınıf → interface → CDS → tablo tipi → yapı/tablo → data element → domain → mesaj sınıfı.
- `isDeleted="false"` dönen obje: harness loglar, koşuyu "kirli" işaretler, sonraki koşu yine benzersiz önekle başlar.
**Sızıntı koruması** (silme başarısız olursa diye, ek güvenlik): Her koşunun öneki benzersizdir. Harness, modelin `sap_search_object` ve `sap_usage_references` sonuçlarından başka koşuların öneklerini filtreler.
**Paralellik (test edildi, 2026-10-01):** MCP server en fazla **8 eşzamanlı oturum** tutuyor. Bu sınır bilinçli: A4H ve ADT'nin taşıyabileceği yük kadar. 9. oturum `initialize`'da ID alıyor ama ilk çağrıda HTTP 404 dönüyor. 8 paralel syntax check 1–2 saniyede bitti. Toplam kapasite 8 oturumla sınırlı; Adım 3 ve 5 (binlerce koşu) için süre hesabı buna göre yapılır. (Karşılaştırma: normal geliştirme sistemlerinde 12 agent aynı anda çalışabiliyor; kısıt A4H'nın kaynakları.)
**Makine dağılımı (v13: her şey Mac mini'de):**
Yük testi, 2026-10-02, Mac mini M4 Pro 64 GB, A4H çalışırken Qwen 3.8 27B (MLX, nvfp4), `num_ctx` 32k:
| Ölçüm | Değer |
|---|---|
| A4H bellek (üretim sırasında) | 28,9 GB (Docker limiti 62,7 GB) |
| Sistem boş bellek / swap | %49 / 0,5 GB — baskı yok |
| A4H tepkisi | Etkilenmedi |
| Prompt eval | 119 token/s |
| Üretim (eval) | 31 token/s |
| Test prompt'u (T01 benzeri, tek sınıf + testler) | 11 678 token, 6 dk 18 sn |
Sonuç: A4H, MCP server, harness ve model Mac mini'de birlikte çalışır. MacBook serbest kalır.
- 11 678 token'ın çoğu muhtemelen düşünme (thinking) çıktısı. Ölçümde iki mod da denenir: thinking açık ve kapalı. Süre ve puan farkı baz model seçimine girer.
- Kaba süre tahmini: görev başına 15 tur × ~2 000 token ≈ 16 dk üretim + tool süresi → 20–30 dk/görev. 100 görev ≈ 35–50 saat/model. Mac mini 7/24 koşarsa model başına ~2 gün.
- Opsiyonel koruma: Docker Desktop bellek limitini ~34 GB'a indirmek; HANA büyüyüp modelin alanına girmesin.
- Adım 3'te kapasite yetmezse: ikinci A4H (kiralık VM) veya modeli kiralık GPU'da koşturmak.
Harness kuralları:
- Yerel model (MacBook): **tek worker**, paralel istek yok. Paralel worker (en fazla 6, 2 oturum senin kullanımın için) sadece model uzakta çalışıyorsa (kiralık GPU).
- Her worker koşu boyunca tek oturum kullanır, koşu sonunda oturumu `DELETE` ile kapatır.
- 404 alan çağrı: oturumu yeniden aç, çağrıyı bir kez tekrarla, koşuyu "kirli" işaretle.
Opsiyonel server iyileştirmesi: sınır aşıldığında `initialize` aşamasında açık hata (429/503) dönmek. Böylece harness oturumu almadan bekler. Paralel **yazma ve aktivasyon** henüz test edilmedi; Adım 1.3'te ölçülecek.
**Genel ABAP MCP arayüzü (karar, 2026-10-02).** Model EPOD tool isimlerini değil, genel bir "ABAP MCP" arayüzünü görür. Ayrıntılı spec: `abap-mcp-arayuz.md` (sonra yazılacak).
| Genel tool | EPOD karşılığı (ilk adapter) |
|---|---|
| `search_object` | `sap_search_object` |
| `read_source` | `sap_pull_source` |
| `object_structure` | `sap_object_structure`, `sap_object_members` |
| `where_used` | `sap_usage_references` |
| `create_object` | `sap_create_object` |
| `write_source` | `sap_push_source`, `sap_push_element` |
| `activate` | `sap_activate` |
| `syntax_check` | `sap_syntax_check` |
| `run_unit_tests` | `sap_run_unit_test`, `sap_check_object` (coverage) |
| `run_atc` | `sap_atc_run` |
- EPOD bu arayüzü native destekleyecek. O zamana kadar harness proxy'si genel arayüzü EPOD tool'larına çeviren **ilk adapter**dir.
- **Delete/teardown arayüzde yok.** Harness'e aittir (ADT deletion API).
- Çekirdek dışındaki EPOD tool'ları (`sap_sql_query`, `sap_run_class`, `sap_short_dumps`, `sap_element_info`, `sap_inactive_objects`, `sap_pretty_print`, `sap_push_message`) için genel karşılık spec'te kararlaştırılacak.
- **Eğitimde az miktarda tool isim ve şema varyasyonu** kullanılır (ör. `read_source` / `get_source`, parametre adlarında farklılık). Amaç: model belirli bir isim setine ezber yapmasın.
**Tool seti (server v2.3.0, 33 tool).** Model sadece aşağıdaki beyaz listeyi görür. Server tool'larında `readOnlyHint` anotasyonu yok; bu yüzden ayrım harness'te beyaz listeyle yapılır.
| Grup | Model görür | Model görmez |
|---|---|---|
| Okuma | `sap_search_object`, `sap_pull_source`, `sap_object_structure`, `sap_object_members`, `sap_usage_references`, `sap_element_info`, `sap_inactive_objects`, `sap_short_dumps`, `sap_sql_query` | `sap_adt_raw_get`, `sap_list_systems`, `sap_compare_systems`, `sap_object_versions`, `sap_object_diff`, `sap_pull_source_to_file` |
| Yazma | `sap_create_object`, `sap_push_source`, `sap_push_element`, `sap_push_message`, `sap_activate` | `sap_push_source_from_file`, `sap_lock`, `sap_unlock` |
| Kalite | `sap_syntax_check`, `sap_check_object` (`runUnitTest`, `coverage`), `sap_run_unit_test`, `sap_atc_run`, `sap_pretty_print` | |
| Çalıştırma | `sap_run_class` (`$TMP` smoke için) | |
| Transport | | `sap_transport_list`, `sap_transport_of_object`, `sap_transport_create`, `sap_transport_release` |
Notlar:
- `sap_create_object` 16 tip oluşturur: PROG, CLAS, INTF, FUGR, FUNC, DTEL, DOMA, TABL, TTYP, STRU, DDLS, DDLX, SRVD, DCLS, DDLA, MSAG. BDEF oluşturamaz → kategori J (RAP) için server'a BDEF create eklenmeli.
- `sap_check_object` P1/P2 ATC bulgusunda başarısız döner; G5 bununla ölçülür. `coverage=true` sadece global sınıfı ölçer.
- `sap_push_source` varsayılan olarak aktive eder. Birden çok objeli değişiklikte `activate=false` + `sap_activate` sırası modelin öğrenmesi gereken bir beceri.
**Aktivasyon kontrolü:** Planlarda geçen "MCP activation wedge" nedeniyle harness, her push sonrası aktif versiyonu ayrıca okur. Modelin raporu değil, aktif kaynak puanlanır.
## 6. Puanlama
### 6.1 Kapılar (0/1)
Bir kapı geçilmezse görev puanı 0'dır.
- G1: Kontrattaki tüm objeler var ve aktif.
- G2: Kontrattaki public imzalar spec ile birebir aynı.
- G3: Gizli testlerin derlenmesi ve en az bir testin geçmesi.
- G4: Out of scope listesindeki hiçbir obje değişmemiş. Harness, seed objelerinin aktif kaynağını koşu öncesi ve sonrası karşılaştırır.
- G5: Açık P1 ATC bulgusu yok.
### 6.2 Bileşenler (kapılar geçildiyse)
| Bileşen | Ağırlık | Ölçüm |
|---|---|---|
| Doğruluk | 40 | Gizli ABAP Unit testlerinin geçme oranı |
| Zanaat | 20 | Görevin `craft_checks` listesi (abaplint kuralları + özel AST kontrolleri) |
| Clean ABAP + Coding standards | 15 | abaplint Clean ABAP kuralları + görevin coding standards kontrolleri |
| Modelin testleri | 15 | (a) Hatalı referans implementasyonu yakalama, (b) statement/branch coverage (hedef görevde verilir, ör. %70), (c) gerçek DB erişimi yok |
| Disiplin | 10 | Gerekçesiz P2 bulgusu yok; iki başarısızlık sonrası durma kuralına uyma; change summary eksiksiz ve doğru; tool çağrısı bütçesi |
Ağırlıklar başlangıç değeri. İlk baz model ölçümünden sonra ayarlanır.
### 6.3 Negatif görevler
Beklenen sonuç `stop` olan görevlerde:
- Model obje oluşturmadan durur ve eksik noktayı adlandırır: 100.
- Model durur ama yanlış noktayı adlandırır: 30.
- Model uygular: 0.
"Doğru noktayı adlandırdı mı" kontrolü: görevde `expected_gap` anahtar kelimeleri + hakem model. Hakem modeli sadece bu kontrol için kullanılır.
### 6.4 Release hedefi
A4H en yeni release'te. Ama gerçek sistemler eski olabilir: senin planlarında "this system does not support method chaining" kısıtı var. Bu yüzden her görev bir `release_target` taşır. abaplint `syntax.version` ayarı (ör. `v702`, `v740sp05`, `v750`) bu hedefi A4H'dan bağımsız denetler. Hedef release'i aşan syntax G1'i düşürür.
### 6.5 abaplint ve standart tipler (test edildi, 2026-10-01)
Test: abaplint 2.120.63, abapGit formatında seed objeleri (yapı, tablo tipi, şeffaf tablo) + bunları kullanan bir sınıf.
| Kontrol | Sonuç |
|---|---|
| Seed tiplerini çözme (alan seviyesine kadar) | ✓ `ls_order-amountx` → "Component not found" |
| Eksik Z tipi | ✓ `ZEVAL_TT_MISSING` → `unknown_types` |
| Standart tipler (`TIMESTAMPL`, `SYUNAME`, `WAERS`, `CL_ABAP_TSTMP`) | Yanlış alarm yok (`errorNamespace: ^(Z|Y|LCL_|TY_|LIF_)`) |
| Standart sınıfta yanlış parametre adı | ✗ Yakalanmıyor (beklenen) |
| `db_operation_in_loop` | ✓ |
| Release hedefi v702: inline `DATA(...)` | ✓ Hata |
Sonuç ve iş bölümü:
- **A4H syntax check / aktivasyon = doğruluk.** Standart API kullanımı burada denetlenir.
- **abaplint = release hedefi, zanaat, stil.** Standart tipleri bilmesi gerekmez.
- Seed objeleri abapGit formatında tutulur; abaplint onları doğrudan okur. Aynı dosyalar A4H'ya MCP ile kurulur.
- Not: v702 hatalarında zincirleme (cascade) hatalar oluşuyor. Release kapısı için hata sayısı değil, "en az bir `check_syntax` hatası var mı" ölçülür.
## 7. Görev şeması
```yaml
id: T03
version: 1
category: C # bkz. bölüm 8
difficulty: 2 # 1-3
release_target: v750
expected_outcome: implement # implement | stop
budget:
max_tool_calls: 60
max_activations: 15
spec: spec.md # bölüm 3 formatında, STE100
seed: # görev başında A4H'ya kurulur
objects: seed/ # abapGit formatında DDIC, sınıflar
data: seed/data.json # tablo içerikleri
faulty_reference: seed/ref_faulty/ # modelin testlerini ölçmek için
hidden_tests: hidden/ # global test sınıfı, kontrata bağlı
craft_checks:
- abaplint: db_operation_in_loop
- custom: lookup_table_is_sorted_or_hashed
expected_gap: [] # sadece stop görevlerinde
```
Gizli testler global test sınıfıdır (`FOR TESTING`). Modelin sınıfına sadece public kontrat üzerinden erişir. Bu yüzden G2 (imza eşleşmesi) kapıdır.
## 8. Kategoriler
| Kod | Kategori | Ölçülen beceri |
|---|---|---|
| A | Saf mantık, yeni sınıf | Dil, OO, Clean ABAP |
| B | Veritabanı erişimi + test double | Open SQL, CDS, `CL_OSQL_TEST_ENVIRONMENT` |
| C | Internal table zanaatı | Tablo tipi, key, erişim yolu |
| D | Exception tasarımı | Class-based exception, T100 mesajları |
| E | Refactoring | Legacy kodu davranışı koruyarak temizleme |
| F | Bilinmeyen obje | Spec'te imzası olmayan seed objesini ADT ile okuyup kullanma |
| G | Release kısıtı | Eski release'e uygun syntax |
| H | Negatif | Eksik veya çelişkili spec'te durma |
| I | Hata düzeltme | Kırmızı test veya short dump'tan kök nedene |
| J | CDS / RAP | View entity, behavior (sonraki dalga) |
| K | Eksik / serbest girdi + farklı tool şeması | Plan formatı olmadan, serbest metin veya eksik görev tanımıyla çalışma; farklı tool isim ve şemalarına uyum. Eksik bilgi kritikse durup sorma (H ile ilişkili) |
Hedef dağılım (beceri kategorisine göre): A 10, B 15, C 15, D 10, E 15, F 10, G 10, H 10, I 5, **K 10** (toplam 110). J ikinci dalgada. K görevleri diğer kategorilerin görevlerinin serbest metin veya farklı şemalı varyantları olarak üretilir.
### 8.1 İkinci boyut: obje tipi
Beceri kategorisi tek başına yetmez. T01–T12'nin hepsi sınıf; gerçek işte function module, rapor ve DDIC de var. Her görev bir beceri kategorisi **ve** bir ana obje tipi taşır.
| Obje tipi | Hedef pay | MCP ile mümkün mü | Gizli test yöntemi |
|---|---|---|---|
| CLAS / INTF | ~35 % | ✓ | Global test sınıfı, public kontrat üzerinden |
| FUGR / FUNC | ~15 % | ✓ (`sap_create_object` FUGR + FUNC) | Global test sınıfı `CALL FUNCTION` ile çağırır; exception'lar `EXCEPTIONS` ile kontrol edilir |
| PROG (rapor, selection screen, ALV) | ~10 % | ✓ | `SUBMIT ... WITH ... AND RETURN` + `cl_salv_bs_runtime_info=>set( display = abap_false data = abap_true )` ile ALV verisi yakalanır; WRITE listeleri `EXPORTING LIST TO MEMORY` + `LIST_FROM_MEMORY` ile |
| DDIC (TABL, STRU, DTEL, DOMA, TTYP) | ~10 % | ✓ | Yapı kontrolü: `sap_object_structure` / `DD03L`, `DD04L`, `DD01L` sorguları; kullanan kodun derlenmesi |
| CDS (DDLS, DCLS, DDLX, SRVD) | ~25 % | ✓ (BDEF hariç) | `CL_CDS_TEST_ENVIRONMENT` ile test double; sonuç kümesi karşılaştırması |
| MSAG + exception | ~5 % | ✓ (`sap_push_message`) | T100 mesaj metni ve parametre kontrolü |
**CDS alt konuları** (25 görev civarı): view entity ve join'ler, association ve path expression, aggregation ve `GROUP BY`, `CASE` / cast / built-in fonksiyonlar, parametreli view, access control (DCLS), annotation ve metadata extension (DDLX), table function + AMDP sınıfı, service definition (SRVD), ABAP'tan CDS tüketimi (Open SQL ile association kullanımı). RAP (BDEF) server'a BDEF oluşturma eklenince J kategorisinde.
Kapsam dışı (ADT/MCP ile yapılamıyor): dynpro (`CALL SCREEN`), SmartForms, BAdI/enhancement implementasyonu, IDoc tanımı. Gerçek işte var ama A4H + MCP ile doğrulanamıyor.
**Gerçek işten uyarlanabilir desen:** FI-CA event modülleri gibi "sabit arayüzlü FM" işleri. Seed'de örnek bir FM (sabit imza) verilir; görev aynı imzayla yeni bir FM yazmaktır. Bu, F kategorisini (bilinmeyen objeyi okuyup taklit etme) gerçek bir desenle ölçer.
**Release ile ilişki:** Eski release hedefli görevler (G kategorisi) doğal olarak FM ve rapor ağırlıklı olur: 7.02'de sınıf yerine FORM rutinleri ve FM'ler yaygın.
**Harness'te gereken değişiklikler:**
- G2 (kontrat): FUNC için imza kontrolü (`FUNCTION ... IMPORTING/EXPORTING/TABLES/EXCEPTIONS` bloğu), PROG için selection screen parametreleri.
- abaplint dışa aktarımı: şu an sadece CLAS/INTF. PROG ve FUGR abapGit formatı eklenecek.
- Teardown sırası: FUNC → FUGR, PROG.
- Seed kurulumu: FUGR + FUNC (FM önce grup ister).
**Sıradaki görevler:** T13 (FUNC, sabit arayüzlü FM, kategori F) ve T14 (PROG, selection screen + ALV, kategori B).
## 9. Örnek görevler
### 9.1 Tam örnek: T03 (kategori C)
**Seed:** `ZEVAL_DISC_RULE` tablosu (`CUST_GROUP` CHAR4 key, `MIN_AMOUNT` CURR, `DISCOUNT_PCT` DEC 5,2), 200 satır. `ZEVAL_S_ORDER` yapısı ve `ZEVAL_TT_ORDER` tablo tipi.
**spec.md:**
```markdown
# 1. Goal
Calculate the discount for each order line. The sales team uses the result in a
nightly batch run with up to 500 000 order lines.
# 2. Open questions
None.
# 3. Context
- Table ZEVAL_DISC_RULE, package ZEVAL_T03. Key: CUST_GROUP.
Fields: CUST_GROUP (CHAR4), MIN_AMOUNT (CURR 15,2), DISCOUNT_PCT (DEC 5,2).
One customer group has one or more rules.
- Table type ZEVAL_TT_ORDER, line type ZEVAL_S_ORDER.
Fields: ORDER_ID (CHAR10), CUST_GROUP (CHAR4), AMOUNT (CURR 15,2),
CURRENCY (CUKY).
# 4. Contract
- Create the class ZCL_EVAL_DISCOUNT_CALC in the package ZEVAL_T03.
- Public method CALCULATE:
IMPORTING it_orders TYPE zeval_tt_order
RETURNING VALUE(rt_result) TYPE zeval_tt_disc_result
- Create the table type ZEVAL_TT_DISC_RESULT. Line fields: ORDER_ID,
DISCOUNT_PCT, DISCOUNT_AMOUNT (CURR 15,2), CURRENCY.
- The method returns one result line for each order line, in the input order.
# 5. Business rules
- For an order line, use the rule of the same customer group with the highest
MIN_AMOUNT that is less than or equal to AMOUNT.
- If no rule applies, the discount is 0.
- DISCOUNT_AMOUNT is AMOUNT multiplied by DISCOUNT_PCT divided by 100. Round
half up to 2 decimals.
# 6. Constraints
- Release target: 7.50.
- Do not change ZEVAL_DISC_RULE.
# 7. Acceptance
- The hidden tests pass.
- The class runs 500 000 order lines without a database access in a loop.
```
**Gizli testler:** kural yok, tek kural, sınır değer (`AMOUNT = MIN_AMOUNT`), çoklu kural, yuvarlama, boş girdi, sıra korunumu.
**craft_checks:** `db_operation_in_loop` (abaplint); özel: kural tablosu `SORTED` veya `HASHED`, ya da tek `SELECT` + sıralı okuma; loop içinde `LOOP AT ... WHERE` üzerinde standart tablo yok.
**faulty_reference:** Sınır değerde `<` yerine `<=` hatası olan implementasyon. Modelin testleri bunu yakalamalı.
### 9.2 Kısa örnekler
| ID | Kat. | Release | Görev özeti | Beklenen zanaat / davranış |
|---|---|---|---|---|
| T01 | A | 7.58 | IBAN biçim doğrulayıcı sınıf; mod-97 kontrolü, hata nedenleri enum olarak | Saf fonksiyonlar, `ENUM` veya sabitler, küçük metotlar |
| T02 | B | 7.58 | Seed tablosundan açık kalemleri yaşlandırma bantlarına göre toplayan sınıf | Tek aggregate `SELECT`, `CL_OSQL_TEST_ENVIRONMENT` ile test |
| T03 | C | 7.50 | Yukarıdaki tam örnek | Loop dışı SELECT, sorted/hashed lookup |
| T04 | D | 7.58 | Dosya satırı parser'ı; satır ve alan bilgisiyle anlamlı hata | Class-based exception, T100 mesaj sınıfı, `IF_T100_DYN_MSG` |
| T05 | E | 7.58 | 300 satırlık seed rapor (FORM'lar, global data) → sınıfa refactor; mevcut çıktı testle sabit | Davranış korunur, global state yok, testlenebilir yapı |
| T06 | F | 7.58 | Seed'deki `ZCL_EVAL_FX_PROVIDER` ile para birimi çevirimi. Spec sadece sınıf adını verir, imzayı vermez | Model önce ADT ile okur, sonra kullanır |
| T07 | G | 7.02 | T03 benzeri lookup, ama 7.02 hedefi | Inline declaration yok, `NEW`/`VALUE`/`COND` yok, method chaining yok |
| T08 | H | 7.58 | Spec iki çelişkili yuvarlama kuralı içeriyor | `stop`; `expected_gap`: rounding |
| T09 | H | 7.58 | Spec bir alan eşlemesini vermiyor (kaynak alan belirsiz) | `stop`; `expected_gap`: field mapping |
| T10 | I | 7.58 | Seed sınıf büyük girdide `TSV_TNEW_PAGE_ALLOC_FAILED` dump'ı veriyor; dump bilgisi spec'te | Kök neden: gereksiz tablo kopyası; düzeltme + regresyon testi |
| T11 | B+F | 7.50 | Nötr alan: seed'de log tablosu + referans tablosu + factory sınıfı. Görev: iki yönlü reader interface ve sınıfı (anahtardan kayda, kayıttan anahtara), factory'ye `GET_READER` ekle. Belge anahtarı `REF TO data` olarak gelir; iki eşleme dalı | Join + en yeni satır (`ORDER BY ... DESCENDING`, `UP TO 1 ROWS`), cast hatası → anlamlı exception, factory'nin mevcut desenini okuyup taklit etme (F), method chaining yok, OSQL test double, `$TMP` smoke ile factory doğrulaması |
| T12 | H | 7.58 | T11'in varyantı: istenen exception text id'si için gereken attribute mevcut exception sınıfında yok | `stop` veya açık sapma raporu; `expected_gap`: exception attribute |
## 10. Harness akışı
```
1. setup paket oluştur, seed objelerini kur, seed verisini yükle
2. run modeli başlat: system prompt + spec + MCP tool'ları
bütçe: max_tool_calls, max_activations, zaman aşımı
3. collect paketteki tüm objelerin aktif kaynağını çek
4. gate G1 (aktif mi), G2 (imza eşleşmesi)
5. test gizli test sınıfını kur, ABAP Unit koştur
6. check ATC + abaplint + özel craft kontrolleri
7. own-tests modelin testlerini faulty_reference'a karşı koştur
8. score puanı hesapla, JSON olarak yaz
9. log tam yörüngeyi JSONL olarak sakla (SFT adayı)
10. teardown paketi sil
```
Harness dili: Python (eğitim ekosistemi aynı dilde). MCP istemcisi olarak EPOD ADT MCP server'ına bağlanır.
Model arayüzü: OpenAI-uyumlu chat API. Böylece Ollama/MLX ile yerel modeller ve uzak API'ler aynı harness'te koşar.
## 11. Baz model ölçümü
Faz 1 sonunda aynı 100 görev şu gruplarla koşar:
- Aday baz modeller (Apache 2.0, orta boy, agentic). Liste ölçüm öncesi güncel durumla seçilir.
- Referans üst sınır: Claude. Sadece ölçüm için; çıktıları eğitim verisine girmez.
Sonuç tablosu: kategori bazında puan. Baz model seçimi ve eğitim önceliği bu tablodan çıkar.
**Hedef çizgi:** Claude referansına tüm kategorilerde en fazla 10 puan geride kalmak. T11 ve T12 bu karşılaştırmanın ana görevleri.
## 11a. Adım 1.3 sonuçları (2026-10-01)
**Konum:** `~/projects/abap-llm/harness` (MacBook)
| Modül | Görev |
|---|---|
| `harness/mcp_client.py` | MCP streamable-http istemcisi; 404'te oturumu yeniden açar |
| `harness/adt_client.py` | ADT deletion API (sadece teardown; kimlik bilgisi `.env`'den) |
| `harness/proxy.py` | Beyaz liste, bütçe, başka koşu öneklerini filtreleme, yazma hata serisi sayacı, yörünge logu |
| `harness/agents.py` | `oracle` (referans çözüm), `null`, `llm` (OpenAI-uyumlu, zaman bütçesi 30 dk) |
| `harness/runner.py` | setup → agent → collect → G1–G6 → gizli testler → kendi testleri → ATC → abaplint → puan → teardown |
| `harness/cli.py` | `run`, `teardown`, `teardown-all`, `cleanup-list` |
| `tasks/T01/` | IBAN görevi: spec, seed interface, 12 gizli test, referans |
**Koşular (T01):**
| Koşu | Agent | Puan | Not |
|---|---|---|---|
| 002 | oracle | 85 | 12/12 gizli test, abaplint ve ATC temiz. Kendi testleri 0 (referansta test yok). |
| 003 | null | 0 | G1, G2, G3 düştü. |
| 004 | qwen3.8:27b-mlx | — (32 dk sonra elle durduruldu; erken) | 15 tool çağrısı, 11 yazmadan 10'u aktivasyon hatası. Interface metodunu niteleme, offset syntax'ı (`lv_pos+1`), `find( ... ignore_case )` parametreleri. İki başarısızlıktan sonra durmadı. |
Oracle ve null, puan aralığının iki ucunu doğruluyor. Koşu 004, eval'in ölçmesi gereken zayıflığı tam gösteriyor: ABAP syntax bilgisi ve durma disiplini.
**Eklenen kurallar:**
- Yerel model kuralları: zaman limiti yok (sınırı tool çağrısı bütçesi koyar; `--max-minutes` opsiyonel). İstekler tek tek gider: tek worker, `parallel_tool_calls: false`, tool çağrıları sırayla çalışır.
- Disiplin: art arda 3 veya daha fazla başarısız yazma/aktivasyon → disiplin puanından −5.
**Ölçüm süresi riski:** Yerel 27B model, Ollama'da 262k bağlamla tur başına 2–3 dakika harcadı. 100 görev × 30 dakika ≈ 50 saat/model. Adım 1.6 öncesi: bağlam penceresini sınırlamak (ör. 32k), MLX server'ı denemek, ölçümü gece ve hafta sonu koşularına bölmek. Koşu 005 (sınırsız süre) ilk gerçek süre ölçümünü verecek.
**MCP server eksiği (test include):** Yeni sınıfta test include yok; ADT artık onu otomatik oluşturmuyor (butona basmak gerekiyor). MCP oluşturamıyor. Karar: server'a eklenecek. Geçiş döneminde model testleri global test sınıfına yazabilir; puanlama ikisini de sayıyor. Eklendikten sonra yeni bir probe sınıfıyla test edilir.
**Teardown:** Mac mini'de `.env` (A4H kimlik bilgisi) dolduruldu; teardown her koşu sonunda otomatik çalışır. Kilitli 1 obje ve MacBook'taki koşu 005'in objeleri (`Z005001_`) bekliyor; koşu 005 bitmeden silinmez.
## 11b. Mac mini kurulumu (2026-10-02)
| Konu | Durum |
|---|---|
| Konum | `~/projects/abap-llm/harness` (kullanıcı `erhankeseli`) |
| Erişim | Bu Project'in sohbetleri Mac mini'deki Claude Desktop'tan; Desktop Commander Mac mini'de |
| MCP server | ADT (Eclipse) içinde, `127.0.0.1:3000`. Sistem adı `A4H`, mod `write` |
| MCP token | ADT plugin ayarından (`com.epod.adt.mcp.plugin.prefs`) `.env`'e yazıldı; `.env` izni 600 |
| Python / Node | Python 3.9.6 (sistem), Node 26.8.2; abaplint 2.120.63 |
| Model | `qwen3.8-27b-32k` = `qwen3.8:27b-mlx` + `num_ctx 32768` (Ollama Modelfile) |
| Bağlam takibi | Her model yanıtının `usage` değeri yörüngeye yazılıyor; 32k sınırına yaklaşma buradan görülür (Ollama sınırı aşınca sessizce keser) |
| Koşu numaraları | MacBook: 001–099, Mac mini: 101+. Önekler çakışmaz. |
Doğrulama: oracle (koşu 101) **85**, null (koşu 102) **0**. MacBook sonuçlarıyla aynı.
### Koşu 103: qwen3.8-27b-32k, T01 — **41,7 / 100**
| Bileşen | Puan | Neden |
|---|---|---|
| Doğruluk | 16,7 / 40 | 12 gizli testten 5'i geçti. Geçerli IBAN'lar OK dönmüyor (checksum hesabı yanlış); COUNTRY ve LENGTH kontrolleri yanlış sırada veya yanlış. |
| Zanaat | 10 / 20 | `method_length`: ana metot 45, yardımcı runner metodu 84 statement. |
| Clean ABAP | 15 / 15 | Bulgu yok. |
| Kendi testleri | 0 / 15 | İki "test" sınıfı yazdı ama `FOR TESTING` yok; biri interface metodunu niteleme hatasıyla derlenmiyor. Gerçek ABAP Unit testi yok. |
| Disiplin | 0 / 10 | Tool bütçesi (40) bitti, 22 aktivasyon (bütçe 10), art arda 5 başarısız yazma. Rapor yazamadan durdu. |
Süre: 38 dakika (2 269 sn). Yardımcı objeler: bir `IF_OO_ADT_CLASSRUN` runner (smoke için; kurala uygun) ve iki test sınıfı denemesi.
**Bu koşudan çıkan harness düzeltmeleri:**
- abaplint JSON çıktısında `file` alanı string geliyor; parse düzeltildi (koşu 103 bu yüzden puanlama adımında çökmüştü).
- `rescore` komutu: agent'ı tekrar çalıştırmadan, A4H'daki objelerden ve yörüngeden yeniden puanlar. Puanlayıcı değiştiğinde eski koşular tekrar değerlendirilebilir.
- G6 (release kapısı) sadece kontrat objelerine uygulanır. Yardımcı objeler abaplint'in bilmediği standart API'leri (`IF_OO_ADT_CLASSRUN`) kullanabilir; bunlarda yanlış alarm oluşuyordu. Doğruluk için A4H syntax check yeterli.
- G4 karşılaştırması normalize edildi (boşluk ve büyük/küçük harf).
- Kendi testleri: modelin yazdığı **global test sınıfları** da sayılır (MCP local test include oluşturamadığı için modelin tek yolu bu).
**Açık puanlama soruları:**
- Yardımcı objelerdeki (runner, smoke) bulgular zanaat puanından düşsün mü? Şu an düşüyor.
- Bütçe bitince rapor yok. Rapor eksikliği ayrıca cezalandırılsın mı, yoksa bütçe aşımı cezası yeterli mi?
## 11c. Baştan başlangıç (2026-10-02, koşu 201+)
**Test include çözüldü (server):** `sap_create_object` CLAS ile test include'u da oluşturuyor; `sap_push_source` `includeType: "testclasses"` ile local test yazıyor (include yoksa oluşturuyor). Probe `Z191001_TINC`: 1 test, coverage %50 (beklenen). Global test sınıfı geçici çözümüne artık gerek yok; puanlama ikisini de saymaya devam ediyor.
**Uygulanan puanlama kararları:**
- Zanaat bulguları sadece kontrat objelerinden sayılır (yardımcı runner/smoke objeleri hariç).
- Model rapor yazmadan biterse (boş veya "Stopped:") disiplinden −5.
**Referans çözüme local testler eklendi** (T01: 6 test). Oracle artık "kendi testleri" bileşenini de doğruluyor.
| Koşu | Agent | Puan | Not |
|---|---|---|---|
| 201 | oracle | **100** | 12/12 gizli test; kendi testleri 6/6, coverage %100; teardown 3/3 otomatik |
| 202 | null | **0** | |
| 203 | qwen3.8-27b-32k | (çalışıyor) | |
| 204 | deepseek-v4.1-flash:cloud (Ollama cloud) | **98,5** | 12/12 gizli test; 16 local test, coverage %98; 8 tool çağrısı, 5 aktivasyon; 2,5 dk. Tek bulgu `prefer_xsdbool`. Rapor plan formatında, kararlar gerekçeli. İlk aktivasyon hatasını (string offset) bir denemede düzeltti. |
Koşu 204 token kullanımı: 8 tur, 104 093 girdi (kümülatif, çoğu cache), 51 830 çıktı (düşünme dahil), en büyük bağlam 16 496. DeepSeek liste fiyatıyla ≈ 0,09 $ / koşu.
Eski koşular `runs/_archive_v1` altında.
## 11d. Adım B: FM, rapor ve CDS desteği (2026-10-02)
**Doğrulanan mekanizmalar (A4H + MCP):** TABL (DDL kaynağı), DDLS, FUGR, FUNC (kaynakta imza), PROG oluşturma ve yazma; seed verisi `IF_OO_ADT_CLASSRUN` sınıfı + `sap_run_class` (`className`); gizli testlerde `CALL FUNCTION` + `EXCEPTIONS`, `SUBMIT ... AND RETURN` + `cl_salv_bs_runtime_info` ile ALV verisi, `CL_CDS_TEST_ENVIRONMENT` ile CDS test double'ları.
**Harness değişiklikleri:**
- Kurulum: FUGR (kaynaksız), FUNC (`functionGroup`), seed sınıfını çalıştırma (`run: true`), test include.
- G2 kontrat: FUNC parametre adı + tipi (FUNCTION başlığında), PROG selection screen parametreleri, DDLS alan adları (`sap_sql_query` kolonları).
- Kendi testleri: PROG içi local test sınıfları; FUNC/DDLS için global test sınıfı; coverage yoksa testler tam puan.
- abaplint: PROG dışa aktarımı; abaplint'in okuyamadığı koşu objeleri (TABL, DDLS, FUNC) hata sayılmaz, doğruluk A4H syntax check'te.
- Silme sırası: CLAS → INTF → PROG → FUNC → FUGR → SRVD/DDLX/DCLS → DDLS → TTYP → TABL → … ; CDS'in STOB kayıtları atlanır.
- Kurulum başarısızsa koşu puanlanmaz (`setup_failed`), objeler silinir.
**Eşzamanlılık bulgusu (önemli):** Server oturumlar arasında tek RFC bağlantısı paylaşıyor. Paralel bir çağrı beklemek yerine `[LOCK] Concurrent call detected` hatası alıyor. Koşu 214'te T14'ün seed tablosu bu yüzden aktive olmadı. Harness artık bu hatada bekleyip tekrar deniyor (en fazla 8 kez); model bu hatayı görmez. Kalıcı çözüm server'da: çağrıları bağlantı başına sıraya almak veya bağlantı havuzu.
**Koşular:**
| Görev | Tip | Oracle | Null | DeepSeek V4.1 Flash |
|---|---|---|---|---|
| T13 | FUNC (sabit arayüz, F) | 100 | 0 | 85: 8/8 gizli test; kendi test sınıfı yazmadı; 47 sn |
| T14 | PROG + ALV (B) | 100 | 0 | 214: seed kurulumu eşzamanlılık hatası → model tabloyu inaktif görüp **doğru şekilde durdu**. 216 (düzeltmeden sonra): **100**, 4 dk |
| T15 | DDLS (B) | 100 | 0 | 215: çalışıyor |
T13'te ATC "slow parameter passing" bulgusu seed arayüzündeki `VALUE()` yüzündendi; seed referans geçişe çevrildi.
## 11e. Pilot sonuçları (2026-10-02, G0002–G0021)
DeepSeek V4.1 Flash, 20 görev, her görev en fazla 3 onarım turu. Sonuçlar: `runs/gen/pilot.json`.
| | Sonuç |
|---|---|
| Kabul (oracle 100, null 0) | **16 / 20 (%80)**. CLAS 7/9, FUNC 2/3, PROG 3/3, DDLS 4/5 |
| İlk denemede kabul | 3 / 20 (G0004, G0007, G0012) |
| LLM çağrısı | 55 (görev başına 2,75) |
| Maliyet (liste fiyatı, üst sınır) | 2,52 $ → **0,126 $ / görev**, **0,16 $ / kabul edilen görev** (tahmin 0,05 $ idi) |
| Çıktı token | ortalama 35k / çağrı (çoğu reasoning). Bir çağrıda 393k token, içerik boş (G0006, ~0,47 $) |
| Süre | ~2 saat (görev başına ~6 dk) |
Başarısız denemelerin nedenleri (oracle koşusu < 100 olan 37 deneme):
| Neden | Sayı | Görevler | Önlem |
|---|---|---|---|
| İsim > 30 karakter (çoğu test metodu) | 6 | G0002, G0003, G0005, G0009, G0010, G0017 | Statik kontrol + prompt kuralı |
| Metot imzasında `TYPE c LENGTH n` / `TYPE p LENGTH n`; SAP sadece "save operation failed" der | 7 | G0005 (3×), G0006 (3×), G0008 | Yerel abaplint `parser_error` ön kontrolü (satır numarasıyla) + prompt kuralı |
| CDS sözdizimi: parametre `default`, UNION anahtar uyumu, eksik anotasyon | 4 | G0020, G0021 | Prompt kuralı |
| Ayrılmış kelime alan adı (HOURS, MODE) | 2 | G0017, G0021 | Statik kontrol + prompt kuralı |
| Parametreli CDS'te G2 her zaman düşüyor (**harness hatası**: `SELECT *` parametresiz çalışmaz) | 3 | G0020 | Runner: SELECT sütun döndürmezse eleman adları kaynaktan okunur. G0020 tek satır düzeltmeyle oracle 100 |
| Tablo seed'i DDLS tipiyle | 2 | G0003 | Statik kontrol + prompt kuralı |
| Geçersiz / boş JSON | 3 | G0002, G0006, G0010 | `raw_decode`, `max_tokens` 100k, boş içerikte yeniden deneme |
| Eski seed kodu 816'da aktive olmuyor (yeni/eski SQL karışık) | 1 | G0016 | Prompt kuralı |
| Referans mantık hatası (gizli test düşüyor), tip uyumsuzluğu, CX_CDS_FAILURE | 6 | G0013, G0014, G0018, G0019, G0015 | Onarım turu çözdü |
| Sözleşmede sınıf var, yerel testi yok → own_tests 7,5 | 2 | G0011 | Statik kontrol: her referans sınıfının `testclasses_file`'ı olmalı (istisna sınıfları hariç) |
Diğer bulgular:
- Onarım geri bildiriminde G2 nedeni yoktu; model sözleşme hatasını göremiyordu. Runner artık `contract_check` yazar, generator bunu geri bildirime ekler.
- Kabul edilen G0013'ün sözleşmesinde test sınıfı var (`REPL_TEST`). Statik kontrol artık bunu reddeder. G0013 incelemede düzeltilmeli.
- Üretilen görevler örnekteki isimleri kopyalıyor (`T02_HIDDEN`, `T14_TEST`). Kozmetik.
- Statik kontrol ve abaplint ön kontrolü SAP'ye gitmez. Pilottaki 37 başarısız denemenin ~18'ini doğrulama koşusundan önce yakalar (tahmin).
## 12. Açık sorular
Cevaplananlar (Adım 1.2):
- ~~MCP yazma tool'ları~~ → bölüm 5.
- ~~A4H release ve abapGit~~ → SAP_BASIS 816 SP01. abapGit standalone kurulu (`ZABAPGIT_STANDALONE`). Standalone sürüm script'le kullanılamaz; seed kurulumu MCP üzerinden yapılır. code pal for ABAP kurulumu için kullanılabilir.
- ~~Eşzamanlı oturum sınırı~~ → 8 (MCP server), bölüm 5.
- ~~abaplint ve seed tipleri~~ → çalışıyor, bölüm 6.5.
Açık olanlar:
- EPOD server istekleri (Kral): (1) yazma başarısız olursa syntax check çalıştırıp mesajlarını döndürsün; şu an sadece "save failed" diyor, model nedeni göremez (2026-10-02). (2) RFC bağlantısı başına kuyruk veya bağlantı havuzu ("Concurrent call detected"). (3) BDEF oluşturma (RAP).
- Genel ABAP MCP arayüzünün spec'i (`abap-mcp-arayuz.md`): tool isimleri, parametre şemaları, çekirdek dışı tool'lar, hata formatı. Sonra yazılacak.
1. ~~Görevleri kim yazar?~~ → Öneri (2026-10-02): DeepSeek V4.1 Flash yazar. İnceleme üç katmanlı, Kral'ın tam incelemesi yok:
- **Otomatik kapılar:** oracle = 100, null = 0, gizli testler bozuk referansta (mutasyon) düşer, her iş kuralı en az bir gizli testle örtülü.
- **Ampirik süzgeç:** Görev 2–3 modelle koşulur. Güçlü model düşük, referans yüksek puan alıyorsa spec belirsiz olabilir → işaretlenir. Herkesin kolayca geçtiği görevler eval'den çıkarılır.
- **Claude incelemesi** (bu Project'te, kontrol listesiyle): spec netliği, kontrat–test uyumu, gerçekçilik. Sadece eval görevleri; eval görevleri eğitim verisine girmez.
- **Kral:** sadece işaretlenen görevler + kategori başına 1 örnek (~10 görev, ~30 dk).
2. Hakem modeli yerel mi, uzak mı?
## 13. Faz 1 bitti sayılır, eğer
- ~~abaplint, A4H seed tiplerini tanıyarak çalışıyor.~~ ✓
- Harness T01–T10'u uçtan uca koşturuyor ve puanlıyor.
- 100 görevlik set yazılmış ve senin incelemenden geçmiş.
- En az iki baz model ve Claude referansı ölçülmüş.

161
docs/yol-haritasi.md Normal file
View File

@@ -0,0 +1,161 @@
# ABAP Danışman Modeli — Yol Haritası
Durum: v19 · 2026-10-02
Şu anki konum: **Adım 1.3**
## Adım 0 — Çerçeve ve kararlar
| Karar | Durum |
|---|---|
| Modelin rolü: teknik ABAP danışmanı, modül bilgisi yok | ✓ |
| Sözleşme: metin olarak ABAP görevi + genel ABAP MCP arayüzü | ✓ (2026-10-02) |
| Girdi: `plan-agent.md` formatı en iyi sonuç veren girdi, zorunlu değil; Kral'ın akışında plan kullanımı değişmez | ✓ (2026-10-02) |
| Tool arayüzü: EPOD isimleri yerine genel ABAP MCP; EPOD native destekleyecek, harness proxy ilk adapter; delete/teardown harness'te | ✓ (2026-10-02) |
| Eğitimde az miktarda tool isim/şema varyasyonu | ✓ (2026-10-02) |
| Ortam: A4H (yazma + doğrulama) | ✓ |
| Mevcut planlar: sadece şablon, eğitim verisi değil | ✓ |
| Eski playbook formatı kullanılmaz | ✓ (2026-10-02) |
| Eğitim verisinde Claude çıktısı yok; eval görevleri eğitim verisine girmez | ✓ (2026-10-02) |
| Lisans: Apache 2.0 | ✓ |
| Baz model: sadece Apache 2.0 veya MIT lisanslı adaylar | ✓ (seçim Adım 2'de) |
Bitti. ✓
## Adım 1 — Eval seti ve harness
Amaç: modeli ölçebilmek. Ölçüm olmadan eğitim kör olur.
| Alt adım | İş | Durum |
|---|---|---|
| 1.1 | Tasarım dokümanı (`faz1-tasarim.md`) | ✓ v3 |
| 1.2 | Teknik doğrulamalar: MCP tool'ları, A4H release, teardown (ADT deletion API), abaplint + seed tipleri, paralellik (8 oturum) | ✓ |
| 1.3 | Harness iskeleti: setup → run → puan → teardown, T01 ile uçtan uca | **✓** — Mac mini'de; test include ve teardown çözüldü. Baştan koşular: oracle 100, null 0, Qwen (koşu 203) çalışıyor |
| 1.3b | Genel ABAP MCP arayüz spec'i (`abap-mcp-arayuz.md`) ve harness proxy'sinin bu arayüze çevrilmesi | Sonra |
| 1.4 | T01–T15: seed, gizli testler, hatalı referans | |
| 1.5 | 110 görevlik set (kategori K dahil: eksik/serbest girdi + farklı tool şeması): üretim + senin incelemen | |
| 1.6 | Ölçüm: aday baz modeller + Claude referansı | |
Bitti sayılır: 110 görev, kategori bazında sonuç tablosu.
## Hacim tahmini (kaba, 2026-10-02)
İlk ölçümlerle kalibre edilecek (koşu süresi, kabul oranı).
| Aşama | Görev | Koşu | Obje (oluşturulup silinen) | Süre |
|---|---|---|---|---|
| Eval seti (Adım 1) | 110 | 110 / model / mod | ~5–8 / koşu → ~700 / ölçüm | Yerel 27B: 35–50 saat / ölçüm |
| Ölçüm turu (Adım 1.6) | 110 | ~3–4 model × 2 mod + Claude ≈ 900 | ~6 000 | Yerel modeller birkaç hafta; uzak modeller birkaç gün |
| Eğitim verisi (Adım 3) | 1 500–2 000 üretilmiş görev | 4 deneme / görev ≈ 6 000–8 000 | ~40 000–50 000 | Yerel 27B ile ~5 000 saat → **yerelde mümkün değil** |
| Hedef çıktı (Adım 3) | | %40 kabul → **~3 000 yörünge** | | |
| RL (Adım 5, opsiyonel) | aynı havuz | on binlerce | yüz binler | Ayrı planlama |
**Teacher maliyeti (DeepSeek V4.1 Flash, Ollama cloud):** T01'de ölçülen ≈ 0,09 $ / koşu (liste fiyatı). T01 kolay bir görev; ortalama görev için 0,15–0,20 $ varsayımı:
| İş | Koşu | Maliyet (liste fiyatı) |
|---|---|---|
| Eval ölçümü, DeepSeek (110 × 2 mod) | 220 | ~35–45 $ |
| Adım 3 (2 000 görev × 2 deneme; T01'deki başarıyla kabul oranı yüksek beklenir) | ~4 000 | ~600–800 $ |
| **Toplam (RL hariç)** | | **~650–850 $** |
Ollama paneli, koşu 204'ün 8 isteği tek tek: 6 × <0,01 $, 1 × 0,01 $, 1 × 0,03 $ → **≈ 0,05–0,08 $ / koşu**. Liste fiyatı tahminine (0,09 $) yakın; Ollama kullanımı liste fiyatına yakın sayıyor. (Ayın toplamındaki düşük ortalama, başka küçük isteklerden geliyor.)
| İş | Koşu / görev | Tahmin |
|---|---|---|
| Pilot (B–E): ~20 görev üretimi + doğrulama + koşular | ~100 istek grubu | ~5–10 $ |
| Eval seti üretimi + ölçüm (110 görev, 2 mod) | ~330 | ~25–40 $ |
| Eğitim görevi üretimi (2 000 görev) | 2 000 | ~100 $ |
| Yörünge üretimi (2 000 × 2 deneme) | ~4 000 | ~400–600 $ |
| **Toplam (RL hariç)** | | **~550–750 $** |
Bütçe: şu an ~50 $, her ay +60 $. B–F ve eval ölçümü bu bütçeye sığar. Yörünge üretimi aylık 60 $ ile 7–10 ay sürer; o aşamada ~500–700 $ ek kredi önerilir. Kesin rakam pilottan sonra.
**Karar (2026-10-02): DeepSeek V4.1 Flash (Ollama cloud) teacher ve görev yazarı olarak hemen kullanılıyor.**
Sonuçlar:
- Aynı anda A4H'da en fazla ~50 obje olur (6 worker × ~8). Toplam sayı büyük ama birikmez; teardown her koşu sonunda.
- **Adım 3 yerel modelle yapılamaz.** Teacher model hızlı ve uzakta olmalı (kiralık GPU veya açık lisanslı modelin API'si). O zaman darboğaz A4H olur: 6 worker, koşu başına ~5 dk → saatte ~70 koşu → 8 000 koşu ≈ 5 gün.
- **Önek şeması yetmez:** `Z<run 3 hane><task 3 hane>_` en fazla 999 koşu. Adım 3 öncesi koşu kimliği base36 4 karaktere çıkarılmalı (~1,7 milyon koşu).
- Uzun vadede A4H'da izlenecekler: disk (HANA log, versiyon geçmişi, silme logları).
## Hızlandırılmış sıra (2026-10-02)
Adım 3 görev ister; görevler el ile yazılırsa darboğaz olur. Bu yüzden ilk iş **görev üreteci**: hem eval setini (1.5) hem eğitim görevlerini (3.1) aynı hat üretir.
| Sıra | İş | Durum |
|---|---|---|
| A | Önek şeması: koşu 4 karakter base36, görev 3 karakter base36 (`Z005P001_`) | ✓ |
| B | Harness: FUGR/FUNC, PROG, DDLS desteği (G2 kontrat kontrolü, abaplint dışa aktarımı, silme sırası) | ✓ T13–T15 oracle 100 / null 0. Server eşzamanlılık sorunu bulundu (harness'te retry) |
| C | Görev üreteci: DeepSeek görev yazar (spec, seed, gizli testler, referans + local testler); kategori ve obje tipi dağılımına göre | ✓ `harness/generator.py`: üretim → yapı + statik kontrol + yerel abaplint → oracle/null doğrulama → en fazla 3 onarım turu. Pilot sonrası iyileştirildi (faz1-tasarim 11e) |
| D | Otomatik görev doğrulama: oracle = 100, null = 0, gizli testler bozuk referansta düşmeli | oracle/null hazır; mutasyon kontrolü sonra |
| E | Pilot: ~20 görev, tüm obje tipleri; kabul oranı ve gerçek maliyet ölçümü | ✓ 16/20 kabul, 0,16 $ / kabul edilen görev (faz1-tasarim 11e). İyileştirilmiş generator henüz denenmedi |
| F | Eval seti: 110 görev; Kral incelemesi. Eğitim havuzu: otomatik doğrulanmış görevler, eval ile çakışmasız | |
| G | Ölçüm (1.6) ve yörünge üretimi (3.2) paralel worker'larla (bulut model: en fazla 6) | |
## Bütçe takvimi (Ollama cloud, 2026-10-02)
Kalan: ~50 $ (12 Ekim'e kadar). 12 Ekim'de +60 $, sonra her ay +60 $. Birim maliyet (ölçülen): DeepSeek koşusu ≈ 0,05–0,08 $; görev üretimi ≈ 0,13 $ / görev, 0,16 $ / kabul edilen görev (pilot, liste fiyatı). Yerel modeller (Qwen vb.) ücretsiz; gece koşuları.
| Dönem | Bütçe | İş | Tahmini harcama |
|---|---|---|---|
| **2–12 Ekim** | ~50 $ | C: görev üreteci. D: otomatik doğrulama. E: pilot (20 görev, üretim + DeepSeek koşusu) | ~5 $ |
| | | F: eval seti — ~150 aday görev üret (~20 $), süz, 110 kalsın; her aday 1–2 DeepSeek koşusu (ampirik süzgeç, ~10–20 $) | ~30–40 $ |
| | | Yerel ölçüm (Qwen ve aday baz modeller, ücretsiz, geceleri) | 0 $ |
| | | Yedek | ~15 $ |
| **12 Ekim – 12 Kasım** | 60 $ | Adım 2: baz model seçimi (yerel ölçüm sonuçlarıyla). Adım 3 pilotu: ~300 eğitim görevi + ~600 yörünge denemesi | ~60 $ |
| | | Pilot sonucu: gerçek kabul oranı ve maliyet → Adım 3'ün tamamı için ek kredi kararı (tahmin ~400–600 $) | |
| **12 Kasım sonrası** | 60 $/ay + ek kredi | Adım 3'ün tamamı, ardından Adım 4 (SFT) | |
Kurallar:
- Harness her bulut isteğinin token kullanımını `runs/ledger.jsonl`'e yazar (liste fiyatı, cache indirimi olmadan = üst sınır). Dönem harcaması `BUDGET_LIMIT_USD`'ye (şu an 45 $) ulaşınca yeni üretim veya bulut koşusu başlamaz. ✓
- Eval görevleri ve eğitim görevleri ayrı havuzlar; eval görevleri eğitim verisine girmez.
- Bulut ve yerel model aynı Ollama sunucusunda kuyrukta bekleyebilir; ölçümler sırasında ikisi aynı anda koşturulmaz.
## Adım 2 — Baz model seçimi
Adım 1.6 tablosundan seçilir. Aday havuzu: sadece Apache 2.0 veya MIT lisanslı modeller. Kriterler: puan, boyut, Mac mini'de çalışabilirlik.
Bitti sayılır: tek baz model seçildi, gerekçesi yazıldı.
## Adım 3 — Eğitim verisi üretimi
| Alt adım | İş |
|---|---|
| 3.1 | Görev üreteci: eval görevleriyle aynı şemada binlerce sentetik görev. Eval görevleriyle çakışma yok. Girdilerin bir kısmı serbest metin; yörüngelerin az bir kısmı farklı tool isim/şemalarıyla. |
| 3.2 | Yörünge üretimi: açık lisanslı teacher model görevleri A4H'da çözer. |
| 3.3 | Filtre: sadece puan eşiğini geçen yörüngeler kalır. Deduplikasyon. |
| 3.4 | Opsiyonel: izinli lisanslı açık ABAP kodu (continued pretraining için). |
Eğitim verisinde mutlaka olmalı (pilot bulguları, 2026-10-02): modellerin sık yaptığı hatalar için görev ve onarım yörüngeleri.
- Metot parametresinde `TYPE c LENGTH n` geçersiz; adlandırılmış tip (`TYPES`) gerekir.
- Ayrılmış kelime alan adı (HOURS, MODE).
- 30 karakterden uzun isimler (özellikle test metotları).
Bitti sayılır: filtrelenmiş yörünge seti hazır, kategori dağılımı dengeli.
## Adım 4 — SFT (LoRA)
| Alt adım | İş |
|---|---|
| 4.1 | Küçük deneme: Mac mini, MLX |
| 4.2 | Gerçek koşu: kiralık GPU |
| 4.3 | Eval: Adım 1 setiyle, baz modelle karşılaştırma |
Bitti sayılır: eğitilmiş model baz modeli anlamlı farkla geçiyor.
## Adım 5 — RL (opsiyonel)
SFT hedefe ulaşmazsa. Ödül: harness puanı (syntax, aktivasyon, testler, abaplint, ATC).
Bitti sayılır: hedef çizgi (Claude referansına ≤ 10 puan fark).
## Adım 6 — Gerçek iş akışında deneme
`/sapplan` planı → model build-agent rolünde → gerçek bir görev. Eval'in yakalamadığı sorunlar burada çıkar.
Bitti sayılır: senin seçtiğin gerçek görevlerde kabul edilebilir sonuç.
## Adım 7 — Yayın
Ağırlıklar, model kartı, lisans, eval sonuçları, genel ABAP MCP arayüz spec'i. Opsiyonel: harness ve eval seti açık kaynak.
Bitti sayılır: yayınlandı.

View File

@@ -6,6 +6,9 @@ import argparse
import json
import os
import re
import shutil
import subprocess
import tempfile
import time
import urllib.request
@@ -15,6 +18,9 @@ from .ledger import add_usage, check_budget
from .runner import Runner
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ABAPLINT = os.path.join(ROOT, "node_modules", ".bin", "abaplint")
MAX_OUT_TOKENS = 100000
ABAPLINT_VERSIONS = {"v702", "v740sp05", "v750", "v758"}
EXAMPLE_FOR = {"CLAS": "T01", "INTF": "T01", "FUNC": "T13", "PROG": "T14", "DDLS": "T15", "TABL": "T15"}
CATEGORIES = {
"A": "pure logic in a new class (language, OO design, Clean ABAP)",
@@ -37,6 +43,18 @@ Rules for the bundle:
The harness replaces it with a run prefix of 9 characters (for example Z005P001_).
Name length after replacement: classes, programs, function modules, CDS entities max 30;
database tables max 16; function groups max 26.
- Every other ABAP name (methods, test methods, local classes, data, types, constants) has max 30
characters. Keep test method names short, for example "rejects_zero_qty".
- Method parameters need a complete type: "TYPE c LENGTH 4" is not allowed in a signature.
Declare a type first (TYPES ty_zone TYPE c LENGTH 4) and use it.
- Field names in tables and CDS views are not SQL reserved words (no HOURS, MODE, ORDER, DATE, COUNT ...).
- CDS: use "define view entity". Parameters have no default value. In a UNION, all branches have the same
key elements and element names. Hidden tests for a view with parameters pass all parameters.
- Legacy seed code (categories E, I) must activate on SAP_BASIS 816: do not mix old and new syntax in one
SQL statement (with new syntax, every host variable needs "@").
- The contract lists only the objects that the hidden tests call. Test classes are never in the contract.
Every reference class (except exception classes) has a "testclasses_file".
- A database table seed has type "TABL" and DDL source "define table ...". Type "DDLS" is only for CDS views.
- All objects are in package $TMP. Do not use transports.
- Do not use SAP application module data (no FI, SD, MM tables). Use generic business domains and
only objects that exist in every ABAP Platform system (language, ABAP SQL, CDS, CL_ABAP_*, CL_SALV_TABLE,
@@ -84,7 +102,8 @@ def bundle_of(task_dir):
def chat(model, messages, base_url):
body = {"model": model, "messages": messages, "temperature": 0.7}
# max_tokens: one pilot call ran to 393k output tokens (reasoning) and returned no content
body = {"model": model, "messages": messages, "temperature": 0.7, "max_tokens": MAX_OUT_TOKENS}
req = urllib.request.Request(f"{base_url}/chat/completions", json.dumps(body).encode(),
{"Content-Type": "application/json", "Authorization": "Bearer none"})
for attempt in range(4):
@@ -92,7 +111,13 @@ def chat(model, messages, base_url):
with urllib.request.urlopen(req, timeout=1800) as r:
data = json.loads(r.read().decode())
add_usage(model, data.get("usage", {}), kind="generate")
return data["choices"][0]["message"].get("content") or ""
content = data["choices"][0]["message"].get("content") or ""
if content.strip():
return content
# empty content (output limit reached in reasoning): retry once more with the next attempt
if attempt == 3:
return ""
continue
except Exception as e: # noqa: BLE001
if getattr(e, "code", 500) < 500 and getattr(e, "code", 500) != 429:
raise
@@ -103,8 +128,109 @@ def chat(model, messages, base_url):
def parse_bundle(text):
text = text.strip()
text = re.sub(r"^```(json)?\s*|\s*```$", "", text)
start, end = text.find("{"), text.rfind("}")
return json.loads(text[start:end + 1])
# raw_decode stops after the first complete object; text after it (a note, a second object) is ignored
return json.JSONDecoder().raw_decode(text[text.find("{"):])[0]
NAME_RE = re.compile(r"^\s*(?:CLASS-)?(?:METHODS|METHOD|DATA|TYPES|CONSTANTS|FORM|CLASS|INTERFACE)\b:?\s+(\w+)",
re.I | re.M)
# reserved words seen in activation errors, plus common SQL keywords
RESERVED = {"HOURS", "MODE", "ORDER", "GROUP", "DATE", "TIME", "VALUE", "USER", "KEY", "COUNT", "SUM", "MIN",
"MAX", "AVG", "SELECT", "FROM", "WHERE", "TABLE", "VIEW", "UNION", "JOIN", "LEVEL", "SIZE", "TYPE",
"INDEX", "CASE", "WHEN", "THEN", "ELSE", "END", "AS", "ON", "BY", "DESC", "ASC", "DAYS", "MINUTES",
"SECONDS", "YEAR", "MONTH", "DAY", "LIMIT", "OFFSET", "PARAMETERS"}
GLOBAL_TEST_CLASS = re.compile(r"\A\s*(?:\*[^\n]*\n|\"[^\n]*\n|\s)*CLASS\s+\S+\s+DEFINITION[^.]*FOR\s+TESTING",
re.I)
DDL_KIND = re.compile(r"^\s*define\s+(?:root\s+)?(table|view|structure)\b", re.I | re.M)
def lint_files(b):
"""Static checks that need no SAP system: identifier length, seed type against DDL source."""
errs = []
for rel, src in b.get("files", {}).items():
if not rel.endswith(".abap"):
continue
for name in set(NAME_RE.findall(src.replace("{{P}}", "Z0000000_").replace("{{p}}", "z0000000_"))):
if len(name) > 30:
errs.append(f"{rel}: name {name} has {len(name)} characters (ABAP max 30)")
t = b.get("task", {})
oos = {n.upper() for n in t.get("out_of_scope", [])}
test_names = {o.get("name", "").upper() for o in t.get("hidden_tests", [])}
for o in t.get("reference", []):
name = o.get("name", "")
if name.upper() in oos:
errs.append(f"reference object {name} is also in out_of_scope; the solution must change it")
if o.get("type") == "CLAS" and GLOBAL_TEST_CLASS.search(b.get("files", {}).get(o.get("file", ""), "")):
test_names.add(name.upper())
for c in t.get("contract", []):
if c.get("name", "").upper() in test_names:
errs.append(f"contract contains the test class {c.get('name')}; only objects that the hidden "
"tests call belong in the contract")
for o in t.get("reference", []):
if (o.get("type") == "CLAS" and o.get("name", "").upper() not in test_names
and not o.get("testclasses_file")
and not re.search(r"INHERITING\s+FROM\s+\S*CX_", b.get("files", {}).get(o.get("file", ""), ""),
re.I)):
errs.append(f"reference class {o.get('name')} has no testclasses_file (local unit tests)")
for rel, src in b.get("files", {}).items():
if DDL_KIND.search(src):
for f in re.findall(r"^\s*(?:key\s+)?(\w+)\s*:", src, re.I | re.M):
if f.upper() in RESERVED:
errs.append(f"{rel}: field name {f} is a reserved word; choose another name")
for k in ("seed", "reference"):
for o in t.get(k, []):
m = DDL_KIND.search(b.get("files", {}).get(o.get("file", ""), ""))
if not m:
continue
want = {"table": "TABL", "structure": "TABL", "view": "DDLS"}[m.group(1).lower()]
if o.get("type") != want:
errs.append(f"{k}: {o.get('name')} has type {o.get('type')}, but its source is "
f"'define {m.group(1)}'; use type {want}")
return errs
def abaplint_files(b):
"""Parser errors of ABAP sources (classes, interfaces, programs) found by local abaplint.
SAP often reports only "save operation failed" for these; abaplint gives the line.
Only parser_error: check_syntax gives false errors for standard objects abaplint does not know."""
t, files = b.get("task", {}), b.get("files", {})
ext = {"CLAS": "clas", "INTF": "intf", "PROG": "prog"}
version = t.get("release_target") if t.get("release_target") in ABAPLINT_VERSIONS else "v758"
d = tempfile.mkdtemp(prefix="genlint_")
origin = {}
try:
os.makedirs(os.path.join(d, "src"))
for k in ("seed", "reference", "hidden_tests"):
for o in t.get(k, []):
if o.get("type") not in ext:
continue
name = re.sub(r"\W", "_", o.get("name", "").replace("{{P}}", "Z0000000_")).lower()
for fk, suffix in (("file", ""), ("testclasses_file", ".testclasses")):
if o.get(fk) in files:
fn = f"{name}.{ext[o['type']]}{suffix}.abap"
origin[fn] = o[fk]
src = files[o[fk]].replace("{{P}}", "Z0000000_").replace("{{p}}", "z0000000_")
open(os.path.join(d, "src", fn), "w").write(src)
if not origin:
return []
cfg = {"global": {"files": "/src/**/*.*"}, "dependencies": [],
"syntax": {"version": version, "errorNamespace": "^(Z|Y)"}, "rules": {"parser_error": True}}
json.dump(cfg, open(os.path.join(d, "abaplint.json"), "w"))
p = subprocess.run([ABAPLINT, "abaplint.json", "-f", "json"], cwd=d, capture_output=True, text=True,
timeout=300)
issues = json.loads(p.stdout or "[]")
except Exception: # noqa: BLE001 the pre-check is optional; SAP validation follows
return []
finally:
shutil.rmtree(d, ignore_errors=True)
out = []
for i in issues:
f = i.get("file", "")
fn = os.path.basename(f.get("filename", "") if isinstance(f, dict) else str(f))
if i.get("key") == "parser_error" and fn in origin:
out.append(f"{origin[fn]} line {i.get('start', {}).get('row')}: syntax error for release {version} "
f"(abaplint): {i.get('description')}")
return out[:20]
def check_bundle(b):
@@ -165,7 +291,8 @@ def write_errors(run_dir):
def failure_summary(rep):
out = {"reference_write_errors": write_errors(rep.get("_dir")),"gates": rep.get("gates"), "score": rep.get("score"), "setup": rep.get("setup"),
out = {"reference_write_errors": write_errors(rep.get("_dir")), "gates": rep.get("gates"),
"contract_check": rep.get("contract_check"), "score": rep.get("score"), "setup": rep.get("setup"),
"atc": rep.get("atc"), "abaplint": rep.get("abaplint"), "own_tests": rep.get("own_tests"),
"hidden_failed": [d for d in rep.get("hidden_tests", {}).get("detail", []) if not d["ok"]],
"hidden_install": rep.get("hidden_tests", {}).get("install")}
@@ -189,11 +316,14 @@ def generate(task_id, pool, object_type, category, difficulty, model, base_url,
messages.append({"role": "assistant", "content": text})
try:
b = parse_bundle(text)
errs = check_bundle(b)
found = {"structure": check_bundle(b), "static": lint_files(b), "abaplint": abaplint_files(b)}
except Exception as e: # noqa: BLE001
b, errs = None, [f"invalid JSON: {e}"]
b, found = None, {"json": [f"invalid JSON: {e}"]}
errs = [e for v in found.values() for e in v]
if errs:
log["attempts"].append({"stage": "bundle", "errors": errs})
# one entry per source, so a batch shows what the checks found before SAP
log["attempts"].append({"stage": "bundle", "errors": errs,
"by_check": {k: len(v) for k, v in found.items() if v}})
messages.append({"role": "user", "content": "Fix these problems and return the full bundle again:\n"
+ "\n".join(errs)})
continue

View File

@@ -14,25 +14,33 @@ PLAN = [
("PROG", "B"), ("PROG", "B"), ("PROG", "E"),
("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"), ("DDLS", "B"),
]
# hard batch after the pilot: the types that failed in the pilot (2026-10-03)
HARD = [("CLAS", "D", None), ("CLAS", "E", "legacy price or freight calculation with packed numbers"),
("DDLS", "B", "view with input parameters"), ("DDLS", "B", "UNION of two tables with a literal source element"),
("FUNC", "A", None)]
DDLS_TOPICS = ["join of two tables and a calculated element", "association and path expression",
"aggregation with CASE and GROUP BY", "view with input parameters",
"UNION of two tables with a literal source element"]
def main():
"""python3 -m harness.pilot FIRST_ID [hard RUN_BASE]"""
load_env(os.path.join(ROOT, ".env"))
first = int(sys.argv[1]) if len(sys.argv) > 1 else 2
hard = len(sys.argv) > 2 and sys.argv[2] == "hard"
run_base = int(sys.argv[3]) if len(sys.argv) > 3 else 1100
name = "hard" if hard else "pilot"
plan = HARD if hard else [(o, c, None) for o, c in PLAN]
results = []
d = 0
for k, (otype, cat) in enumerate(PLAN):
for k, (otype, cat, topic) in enumerate(plan):
tid = f"G{first + k:04d}"
topic = None
if otype == "DDLS":
if otype == "DDLS" and not hard:
topic, d = DDLS_TOPICS[d], d + 1
try:
log = generate(tid, "eval", otype, cat, 2, "deepseek-v4.1-flash:cloud",
os.environ.get("LLM_BASE_URL", "http://127.0.0.1:11434/v1"),
1100 + 10 * k, topic)
run_base + 10 * k, topic)
except BudgetExceeded as e:
print("BUDGET", e, flush=True)
break
@@ -41,7 +49,7 @@ def main():
log["spent_total"] = spent()
results.append(log)
print(json.dumps(log), flush=True)
json.dump(results, open(os.path.join(ROOT, "runs", "gen", "pilot.json"), "w"), indent=1)
json.dump(results, open(os.path.join(ROOT, "runs", "gen", f"{name}.json"), "w"), indent=1)
if __name__ == "__main__":

View File

@@ -23,6 +23,22 @@ DELETE_ORDER = ["CLAS", "INTF", "PROG", "FUNC", "FUGR", "SRVD", "DDLX", "DCLS",
SOURCE_TYPES = ("CLAS", "INTF", "PROG", "FUNC", "DDLS", "DCLS", "DDLX", "TABL")
def _cds_elements(src):
"""Element names of a CDS view select list (alias, else last path component). Used when a
SELECT on the view is not possible, for example a view with parameters."""
body = re.sub(r"//[^\n]*|/\*.*?\*/", "", src or "", flags=re.S)
m = re.search(r"\{(.*)\}", body, re.S)
out = set()
for el in re.split(r",(?![^()]*\))", m.group(1) if m else ""):
el = re.sub(r"@\S+(\s*:\s*('[^']*'|\S+))?", "", el).strip()
if not el or el.lower().startswith("_") and " as " not in el.lower():
continue
alias = re.search(r"\bas\s+(\w+)\s*$", el, re.I)
name = alias.group(1) if alias else re.split(r"[.\s]", el.split()[-1])[-1]
out.add(name.upper())
return out
def _obj_args(otype, name, fg=None):
a = {"objectType": otype, "objectName": name}
if fg:
@@ -222,26 +238,36 @@ class Runner:
g1.append({"name": c["name"], "ok": ok})
g["G1_active"] = all(x["ok"] for x in g1) and bool(g1)
g2 = True
g2_detail = []
for c in contract:
src = sources.get((c["type"], c["name"].upper()), "")
if c.get("implements") and not re.search(rf"INTERFACES\s+{re.escape(c['implements'])}\b", src, re.I):
g2 = False
g2_detail.append(f"{c['name']}: does not implement {c['implements']}")
if c["type"] == "FUNC": # signature: every parameter with its type in the FUNCTION header
header = src.split(".", 1)[0]
for prm in c.get("params", []):
if not re.search(rf"\b{re.escape(prm['name'])}\b\)?\s+TYPE\s+{re.escape(prm['type'])}\b",
header, re.I):
g2 = False
g2_detail.append(f"{c['name']}: no parameter {prm['name']} TYPE {prm['type']} "
"in the FUNCTION header")
if c["type"] == "PROG": # selection screen parameters
for prm in c.get("parameters", []):
if not re.search(rf"(PARAMETERS|SELECT-OPTIONS)\s*:?[^.]*\b{re.escape(prm)}\b", src, re.I):
g2 = False
g2_detail.append(f"{c['name']}: no selection screen parameter {prm}")
if c["type"] == "DDLS" and c.get("fields") and g["G1_active"]:
_, q = mcp.call("sap_sql_query", {"query": f"SELECT * FROM {c['name']}", "maxRows": 1})
cols = {col.get("name", "").upper() for col in (_json(q) or {}).get("columns", [])}
if not {f.upper() for f in c["fields"]} <= cols:
if not cols: # view with parameters: SELECT without parameters fails
cols = _cds_elements(src)
missing = {f.upper() for f in c["fields"]} - cols
if missing:
g2 = False
g2_detail.append(f"{c['name']}: CDS elements missing: {sorted(missing)}")
g["G2_contract"] = g2 and g["G1_active"]
rep["contract_check"] = {"g1": g1, "g2": g2_detail}
g["G4_out_of_scope"] = all(
_norm(self._source(mcp, o["type"], o["name"], o.get("functionGroup")))
== _norm(seed_src[o["name"].upper()])

View File

@@ -0,0 +1,29 @@
{
"id": "G0003",
"pool": "eval",
"object_type": "CLAS",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": null,
"null": null
},
{
"stage": "validate",
"oracle": null,
"null": null
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,119 @@
CLASS {{P}}T02_HIDDEN DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA mo_environment TYPE REF TO if_osql_test_environment.
DATA mo_cut TYPE REF TO {{P}}COURSE_BOOKING.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS fill_test_data.
METHODS free_seats_open_course FOR TESTING.
METHODS free_seats_never_neg FOR TESTING.
METHODS free_seats_unknown FOR TESTING.
METHODS bookable_exact_free FOR TESTING.
METHODS not_bookable_too_many FOR TESTING.
METHODS not_bookable_unknown FOR TESTING.
METHODS not_bookable_zero_seats FOR TESTING.
METHODS list_filter_cat_seats FOR TESTING.
METHODS list_sorted_by_course FOR TESTING.
METHODS list_empty_without_match FOR TESTING.
METHODS list_excludes_full FOR TESTING.
ENDCLASS.
CLASS {{P}}T02_HIDDEN IMPLEMENTATION.
METHOD class_setup.
mo_environment = cl_osql_test_environment=>create( VALUE #( ( '{{P}}COURSE' ) ) ).
ENDMETHOD.
METHOD class_teardown.
mo_environment->destroy( ).
ENDMETHOD.
METHOD setup.
mo_environment->clear_doubles( ).
fill_test_data( ).
mo_cut = NEW {{P}}COURSE_BOOKING( ).
ENDMETHOD.
METHOD fill_test_data.
DATA lt_course TYPE STANDARD TABLE OF {{P}}COURSE WITH EMPTY KEY.
lt_course = VALUE #(
( mandt = sy-mandt course_id = 'ABAP101' category = 'IT' title = 'ABAP Basics' capacity = 20 booked = 5 )
( mandt = sy-mandt course_id = 'ABAP102' category = 'IT' title = 'ABAP Advanced' capacity = 10 booked = 10 )
( mandt = sy-mandt course_id = 'ABAP103' category = 'IT' title = 'ABAP Expert' capacity = 12 booked = 15 )
( mandt = sy-mandt course_id = 'SQL101' category = 'DB' title = 'SQL Basics' capacity = 30 booked = 10 )
( mandt = sy-mandt course_id = 'SQL102' category = 'DB' title = 'SQL Tuning' capacity = 8 booked = 8 )
( mandt = sy-mandt course_id = 'HR101' category = 'HR' title = 'HR Basics' capacity = 6 booked = 1 ) ).
INSERT {{P}}COURSE FROM TABLE @lt_course.
ENDMETHOD.
METHOD free_seats_open_course.
cl_abap_unit_assert=>assert_equals( exp = 15
act = mo_cut->get_free_seats( 'ABAP101' )
msg = 'ABAP101 must have 15 free seats' ).
ENDMETHOD.
METHOD free_seats_never_neg.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_free_seats( 'ABAP102' )
msg = 'full course' ).
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_free_seats( 'ABAP103' )
msg = 'overbooked course' ).
ENDMETHOD.
METHOD free_seats_unknown.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_free_seats( 'XX999' ) ).
ENDMETHOD.
METHOD bookable_exact_free.
cl_abap_unit_assert=>assert_true( mo_cut->is_bookable( iv_course_id = 'SQL101' iv_seats = 20 ) ).
ENDMETHOD.
METHOD not_bookable_too_many.
cl_abap_unit_assert=>assert_false( mo_cut->is_bookable( iv_course_id = 'SQL101' iv_seats = 21 ) ).
ENDMETHOD.
METHOD not_bookable_unknown.
cl_abap_unit_assert=>assert_false( mo_cut->is_bookable( iv_course_id = 'XX999' iv_seats = 1 ) ).
ENDMETHOD.
METHOD not_bookable_zero_seats.
cl_abap_unit_assert=>assert_false( mo_cut->is_bookable( iv_course_id = 'SQL101' iv_seats = 0 ) ).
cl_abap_unit_assert=>assert_false( mo_cut->is_bookable( iv_course_id = 'SQL101' iv_seats = -5 ) ).
ENDMETHOD.
METHOD list_filter_cat_seats.
DATA(lt_result) = mo_cut->list_bookable_courses( iv_category = 'DB' iv_seats = 5 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_result ) ).
cl_abap_unit_assert=>assert_equals( exp = 'SQL101' act = lt_result[ 1 ]-course_id ).
cl_abap_unit_assert=>assert_equals( exp = 'SQL Basics' act = lt_result[ 1 ]-title ).
cl_abap_unit_assert=>assert_equals( exp = 20 act = lt_result[ 1 ]-free_seats ).
ENDMETHOD.
METHOD list_sorted_by_course.
DATA(lt_result) = mo_cut->list_bookable_courses( iv_category = 'IT' iv_seats = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = lines( lt_result ) ).
cl_abap_unit_assert=>assert_equals( exp = 'ABAP101' act = lt_result[ 1 ]-course_id ).
cl_abap_unit_assert=>assert_equals( exp = 'ABAP102' act = lt_result[ 2 ]-course_id ).
cl_abap_unit_assert=>assert_equals( exp = 'ABAP103' act = lt_result[ 3 ]-course_id ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lt_result[ 3 ]-free_seats ).
ENDMETHOD.
METHOD list_empty_without_match.
DATA(lt_hr) = mo_cut->list_bookable_courses( iv_category = 'HR' iv_seats = 6 ).
cl_abap_unit_assert=>assert_initial( lt_hr ).
DATA(lt_unknown) = mo_cut->list_bookable_courses( iv_category = 'XX' iv_seats = 1 ).
cl_abap_unit_assert=>assert_initial( lt_unknown ).
ENDMETHOD.
METHOD list_excludes_full.
DATA(lt_result) = mo_cut->list_bookable_courses( iv_category = 'IT' iv_seats = 1 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_result ) ).
cl_abap_unit_assert=>assert_equals( exp = 'ABAP101' act = lt_result[ 1 ]-course_id ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,94 @@
CLASS {{P}}COURSE_BOOKING DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES ty_course_id TYPE c LENGTH 10.
TYPES ty_category TYPE c LENGTH 10.
TYPES:
BEGIN OF ty_course_info,
course_id TYPE ty_course_id,
title TYPE c LENGTH 40,
free_seats TYPE i,
END OF ty_course_info.
TYPES tt_course_info TYPE STANDARD TABLE OF ty_course_info WITH EMPTY KEY.
METHODS get_free_seats
IMPORTING iv_course_id TYPE ty_course_id
RETURNING VALUE(rv_free_seats) TYPE i.
METHODS is_bookable
IMPORTING iv_course_id TYPE ty_course_id
iv_seats TYPE i
RETURNING VALUE(rv_bookable) TYPE abap_bool.
METHODS list_bookable_courses
IMPORTING iv_category TYPE ty_category
iv_seats TYPE i
RETURNING VALUE(rt_courses) TYPE tt_course_info.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_course,
course_id TYPE ty_course_id,
title TYPE c LENGTH 40,
capacity TYPE i,
booked TYPE i,
END OF ty_course.
METHODS read_course
IMPORTING iv_course_id TYPE ty_course_id
RETURNING VALUE(rs_course) TYPE ty_course.
METHODS calculate_free_seats
IMPORTING is_course TYPE ty_course
RETURNING VALUE(rv_free) TYPE i.
ENDCLASS.
CLASS {{P}}COURSE_BOOKING IMPLEMENTATION.
METHOD get_free_seats.
rv_free_seats = calculate_free_seats( read_course( iv_course_id ) ).
ENDMETHOD.
METHOD is_bookable.
IF iv_seats < 1.
rv_bookable = abap_false.
RETURN.
ENDIF.
rv_bookable = xsdbool( iv_seats <= calculate_free_seats( read_course( iv_course_id ) ) ).
ENDMETHOD.
METHOD list_bookable_courses.
DATA lt_course TYPE STANDARD TABLE OF ty_course WITH EMPTY KEY.
SELECT course_id, title, capacity, booked
FROM {{P}}COURSE
WHERE category = @iv_category
INTO TABLE @lt_course.
LOOP AT lt_course INTO DATA(ls_course).
DATA(lv_free) = calculate_free_seats( ls_course ).
IF lv_free < iv_seats.
CONTINUE.
ENDIF.
rt_courses = VALUE #( BASE rt_courses
( course_id = ls_course-course_id
title = ls_course-title
free_seats = lv_free ) ).
ENDLOOP.
SORT rt_courses BY course_id.
ENDMETHOD.
METHOD read_course.
SELECT SINGLE course_id, title, capacity, booked
FROM {{P}}COURSE
WHERE course_id = @iv_course_id
INTO @rs_course.
ENDMETHOD.
METHOD calculate_free_seats.
rv_free = is_course-capacity - is_course-booked.
IF rv_free < 0.
rv_free = 0.
ENDIF.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,63 @@
CLASS ltc_course_booking DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA mo_environment TYPE REF TO if_osql_test_environment.
DATA mo_cut TYPE REF TO {{P}}COURSE_BOOKING.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS setup.
METHODS fill_test_data.
METHODS free_seats_open_course FOR TESTING.
METHODS free_seats_full_course FOR TESTING.
METHODS bookable_enough_seats FOR TESTING.
METHODS not_bookable_full FOR TESTING.
METHODS list_matching_courses FOR TESTING.
ENDCLASS.
CLASS ltc_course_booking IMPLEMENTATION.
METHOD class_setup.
mo_environment = cl_osql_test_environment=>create( VALUE #( ( '{{P}}COURSE' ) ) ).
ENDMETHOD.
METHOD class_teardown.
mo_environment->destroy( ).
ENDMETHOD.
METHOD setup.
mo_environment->clear_doubles( ).
fill_test_data( ).
mo_cut = NEW {{P}}COURSE_BOOKING( ).
ENDMETHOD.
METHOD fill_test_data.
DATA lt_course TYPE STANDARD TABLE OF {{P}}COURSE WITH EMPTY KEY.
lt_course = VALUE #(
( mandt = sy-mandt course_id = 'ABAP101' category = 'IT' title = 'ABAP Basics' capacity = 20 booked = 5 )
( mandt = sy-mandt course_id = 'SQL101' category = 'DB' title = 'SQL Basics' capacity = 30 booked = 30 ) ).
INSERT {{P}}COURSE FROM TABLE @lt_course.
ENDMETHOD.
METHOD free_seats_open_course.
cl_abap_unit_assert=>assert_equals( exp = 15 act = mo_cut->get_free_seats( 'ABAP101' ) ).
ENDMETHOD.
METHOD free_seats_full_course.
cl_abap_unit_assert=>assert_equals( exp = 0 act = mo_cut->get_free_seats( 'SQL101' ) ).
ENDMETHOD.
METHOD bookable_enough_seats.
cl_abap_unit_assert=>assert_true( mo_cut->is_bookable( iv_course_id = 'ABAP101' iv_seats = 10 ) ).
ENDMETHOD.
METHOD not_bookable_full.
cl_abap_unit_assert=>assert_false( mo_cut->is_bookable( iv_course_id = 'SQL101' iv_seats = 1 ) ).
ENDMETHOD.
METHOD list_matching_courses.
DATA(lt_result) = mo_cut->list_bookable_courses( iv_category = 'IT' iv_seats = 1 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_result ) ).
cl_abap_unit_assert=>assert_equals( exp = 'ABAP101' act = lt_result[ 1 ]-course_id ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,8 @@
define table {{P}}COURSE {
key mandt : abap.clnt not null;
key course_id : abap.char(10) not null;
category : abap.char(10);
title : abap.char(40);
capacity : abap.int4;
booked : abap.int4;
}

View File

@@ -0,0 +1,9 @@
@EndUserText.label : 'Training courses'
define table {{P}}COURSE {
key mandt : abap.clnt not null;
key course_id : abap.char(10) not null;
category : abap.char(10);
title : abap.char(40);
capacity : abap.int4;
booked : abap.int4;
}

View File

@@ -0,0 +1,13 @@
@EndUserText.label : 'Training courses'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table {{P}}COURSE {
key mandt : abap.clnt not null;
key course_id : abap.char(10) not null;
category : abap.char(10);
title : abap.char(40);
capacity : abap.int4;
booked : abap.int4;
}

View File

@@ -0,0 +1,24 @@
CLASS {{P}}SEED_COURSES DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS {{P}}SEED_COURSES IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DELETE FROM {{P}}COURSE.
DATA lt_course TYPE STANDARD TABLE OF {{P}}COURSE WITH EMPTY KEY.
lt_course = VALUE #(
( mandt = sy-mandt course_id = 'ABAP101' category = 'IT' title = 'ABAP Basics' capacity = 20 booked = 5 )
( mandt = sy-mandt course_id = 'ABAP102' category = 'IT' title = 'ABAP Advanced' capacity = 10 booked = 10 )
( mandt = sy-mandt course_id = 'ABAP103' category = 'IT' title = 'ABAP Expert' capacity = 12 booked = 15 )
( mandt = sy-mandt course_id = 'SQL101' category = 'DB' title = 'SQL Basics' capacity = 30 booked = 10 )
( mandt = sy-mandt course_id = 'SQL102' category = 'DB' title = 'SQL Tuning' capacity = 8 booked = 8 )
( mandt = sy-mandt course_id = 'HR101' category = 'HR' title = 'HR Basics' capacity = 6 booked = 1 ) ).
INSERT {{P}}COURSE FROM TABLE @lt_course.
out->write( |Courses written: { sy-dbcnt }| ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,68 @@
# 1. Goal
A training provider sells seats in courses. The booking program needs one service that
answers three questions:
- How many seats are free in a course?
- Can we book a given number of seats in a course?
- Which courses of a category still have enough free seats?
# 2. Open questions
None.
# 3. Context
- The database table {{P}}COURSE exists in package $TMP. It has these columns:
MANDT (client), COURSE_ID (CHAR 10, key), CATEGORY (CHAR 10), TITLE (CHAR 40),
CAPACITY (INT4), BOOKED (INT4).
- CAPACITY is the number of seats of the course. BOOKED is the number of seats that
are already sold.
- The class {{P}}SEED_COURSES fills the table with example data. It is active.
# 4. Contract
- Create the class {{P}}COURSE_BOOKING in package $TMP.
- The class is public, final, and has a public constructor without parameters.
- The class has these public types:
- TY_COURSE_ID, type C, length 10.
- TY_CATEGORY, type C, length 10.
- TY_COURSE_INFO, a structure with the components COURSE_ID (type TY_COURSE_ID),
TITLE (type C, length 40) and FREE_SEATS (type I).
- TT_COURSE_INFO, a standard table of TY_COURSE_INFO with an empty key.
- The class has these public methods:
- GET_FREE_SEATS, importing IV_COURSE_ID type TY_COURSE_ID,
returning RV_FREE_SEATS type I.
- IS_BOOKABLE, importing IV_COURSE_ID type TY_COURSE_ID and IV_SEATS type I,
returning RV_BOOKABLE type ABAP_BOOL.
- LIST_BOOKABLE_COURSES, importing IV_CATEGORY type TY_CATEGORY and IV_SEATS type I,
returning RT_COURSES type TT_COURSE_INFO.
- Do not add other public methods.
# 5. Business rules
Free seats
1. Read the course with the course ID.
2. If the table has no course with this ID, the free seats are 0.
3. The free seats are the capacity minus the booked seats.
4. If the capacity minus the booked seats is less than 0, the free seats are 0.
GET_FREE_SEATS returns the free seats of the course.
IS_BOOKABLE
5. If IV_SEATS is less than 1, the result is false.
6. If the table has no course with this ID, the result is false.
7. The result is true if IV_SEATS is less than or equal to the free seats of the
course. Else the result is false.
LIST_BOOKABLE_COURSES
8. Read all courses with the category IV_CATEGORY.
9. Use only the courses whose free seats are greater than or equal to IV_SEATS.
10. Sort the result in ascending order by COURSE_ID.
11. Each result row has the COURSE_ID, the TITLE and the free seats of the course.
12. If no course matches, the result is empty.
# 6. Constraints
- Release target: 8.16.
- Coding standards: Clean ABAP. Method length below 40 statements. No global variables.
No comment that restates the code.
- Out of scope: do not change the table {{P}}COURSE and the class {{P}}SEED_COURSES.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,58 @@
{
"id": "G0003",
"category": "B",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "TABL",
"name": "{{P}}COURSE",
"file": "seed/course.tabl.asddls",
"description": "Training course table with capacity and booked seats"
},
{
"type": "CLAS",
"name": "{{P}}SEED_COURSES",
"file": "seed/seed_courses.clas.abap",
"description": "Fills the course table with example data",
"run": true
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}COURSE_BOOKING"
}
],
"out_of_scope": [
"{{P}}COURSE",
"{{P}}SEED_COURSES"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}COURSE_BOOKING",
"file": "reference/course_booking.clas.abap",
"description": "Course booking service",
"testclasses_file": "reference/course_booking.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"db_operation_in_loop"
]
}

View File

@@ -0,0 +1,14 @@
{
"id": "G0004",
"pool": "eval",
"object_type": "CLAS",
"category": "C",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,207 @@
CLASS {{p}}stock_age_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CONSTANTS c_key_date TYPE dats VALUE '20240401'.
DATA mo_cut TYPE REF TO {{p}}if_stock_age.
METHODS setup.
METHODS gr_date_for_age
IMPORTING iv_age TYPE i
RETURNING VALUE(rv_date) TYPE dats.
METHODS empty_input FOR TESTING.
METHODS single_batch_bucket_01 FOR TESTING.
METHODS bucket_boundaries FOR TESTING.
METHODS groups_sum_quantity_and_count FOR TESTING.
METHODS result_is_sorted FOR TESTING.
METHODS invalid_rows_are_ignored FOR TESTING.
METHODS negative_and_zero_quantity FOR TESTING.
METHODS total_groups_by_material FOR TESTING.
METHODS total_is_sorted FOR TESTING.
METHODS total_ignores_invalid_rows FOR TESTING.
ENDCLASS.
CLASS {{p}}stock_age_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}stock_age( ).
ENDMETHOD.
METHOD gr_date_for_age.
rv_date = c_key_date - iv_age.
ENDMETHOD.
METHOD empty_input.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch( )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_initial( act = lt_result ).
ENDMETHOD.
METHOD single_batch_bucket_01.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-1' batch = 'B1' quantity = 10
gr_date = gr_date_for_age( 0 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 1 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-material
exp = 'MAT-1' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d0_30 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 10 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 1 ).
ENDMETHOD.
METHOD bucket_boundaries.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-B' batch = 'B1' quantity = 1 gr_date = gr_date_for_age( 30 ) )
( material = 'MAT-B' batch = 'B2' quantity = 1 gr_date = gr_date_for_age( 31 ) )
( material = 'MAT-B' batch = 'B3' quantity = 1 gr_date = gr_date_for_age( 60 ) )
( material = 'MAT-B' batch = 'B4' quantity = 1 gr_date = gr_date_for_age( 61 ) )
( material = 'MAT-B' batch = 'B5' quantity = 1 gr_date = gr_date_for_age( 90 ) )
( material = 'MAT-B' batch = 'B6' quantity = 1 gr_date = gr_date_for_age( 91 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 4 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d0_30 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 1 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d31_60 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-batch_count exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 3 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d61_90 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 3 ]-batch_count exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 4 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d91_plus ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 4 ]-batch_count exp = 1 ).
ENDMETHOD.
METHOD groups_sum_quantity_and_count.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-A' batch = 'B1' quantity = 10 gr_date = gr_date_for_age( 1 ) )
( material = 'MAT-A' batch = 'B2' quantity = 5 gr_date = gr_date_for_age( 2 ) )
( material = 'MAT-A' batch = 'B3' quantity = 2 gr_date = gr_date_for_age( 200 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d0_30 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 15 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d91_plus ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-quantity exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-batch_count exp = 1 ).
ENDMETHOD.
METHOD result_is_sorted.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-C' batch = 'B1' quantity = 1 gr_date = gr_date_for_age( 200 ) )
( material = 'MAT-A' batch = 'B2' quantity = 1 gr_date = gr_date_for_age( 40 ) )
( material = 'MAT-B' batch = 'B3' quantity = 1 gr_date = gr_date_for_age( 1 ) )
( material = 'MAT-A' batch = 'B4' quantity = 1 gr_date = gr_date_for_age( 1 ) )
( material = 'MAT-C' batch = 'B5' quantity = 1 gr_date = gr_date_for_age( 1 ) )
( material = 'MAT-B' batch = 'B6' quantity = 1 gr_date = gr_date_for_age( 70 ) ) )
iv_key_date = c_key_date ).
DATA(lt_expected) = VALUE {{p}}if_stock_age=>tt_age_row(
( material = 'MAT-A' bucket = {{p}}if_stock_age=>c_bucket-d0_30 )
( material = 'MAT-A' bucket = {{p}}if_stock_age=>c_bucket-d31_60 )
( material = 'MAT-B' bucket = {{p}}if_stock_age=>c_bucket-d0_30 )
( material = 'MAT-B' bucket = {{p}}if_stock_age=>c_bucket-d61_90 )
( material = 'MAT-C' bucket = {{p}}if_stock_age=>c_bucket-d0_30 )
( material = 'MAT-C' bucket = {{p}}if_stock_age=>c_bucket-d91_plus ) ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 6 ).
DATA lv_index TYPE i.
LOOP AT lt_expected INTO DATA(ls_expected).
lv_index = lv_index + 1.
cl_abap_unit_assert=>assert_equals( act = lt_result[ lv_index ]-material
exp = ls_expected-material ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ lv_index ]-bucket
exp = ls_expected-bucket ).
ENDLOOP.
ENDMETHOD.
METHOD invalid_rows_are_ignored.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-A' batch = 'B1' quantity = 10 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-A' batch = 'B2' quantity = 99 gr_date = '00000000' )
( material = 'MAT-A' batch = 'B3' quantity = 99 gr_date = gr_date_for_age( -5 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 1 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 10 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 1 ).
ENDMETHOD.
METHOD negative_and_zero_quantity.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-A' batch = 'B1' quantity = 10 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-A' batch = 'B2' quantity = -4 gr_date = gr_date_for_age( 6 ) )
( material = 'MAT-B' batch = 'B3' quantity = 5 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-B' batch = 'B4' quantity = -5 gr_date = gr_date_for_age( 6 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-material exp = 'MAT-A' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 6 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-material exp = 'MAT-B' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-quantity exp = 0 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-batch_count exp = 2 ).
ENDMETHOD.
METHOD total_groups_by_material.
DATA(lt_result) = mo_cut->total_by_material(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-A' batch = 'B1' quantity = 10 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-A' batch = 'B2' quantity = 5 gr_date = gr_date_for_age( 100 ) )
( material = 'MAT-B' batch = 'B3' quantity = 7 gr_date = gr_date_for_age( 5 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-material exp = 'MAT-A' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 15 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-batch_count exp = 2 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-material exp = 'MAT-B' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-quantity exp = 7 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-batch_count exp = 1 ).
ENDMETHOD.
METHOD total_is_sorted.
DATA(lt_result) = mo_cut->total_by_material(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-Z' batch = 'B1' quantity = 1 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-A' batch = 'B2' quantity = 1 gr_date = gr_date_for_age( 5 ) )
( material = 'MAT-M' batch = 'B3' quantity = 1 gr_date = gr_date_for_age( 5 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lines( lt_result ) exp = 3 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-material exp = 'MAT-A' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-material exp = 'MAT-M' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 3 ]-material exp = 'MAT-Z' ).
ENDMETHOD.
METHOD total_ignores_invalid_rows.
DATA(lt_result) = mo_cut->total_by_material(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-A' batch = 'B1' quantity = 99 gr_date = '00000000' )
( material = 'MAT-A' batch = 'B2' quantity = 99 gr_date = gr_date_for_age( -1 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_initial( act = lt_result ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,92 @@
CLASS {{p}}stock_age DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_stock_age.
PRIVATE SECTION.
TYPES tt_age_group TYPE HASHED TABLE OF {{p}}if_stock_age=>ty_age_row
WITH UNIQUE KEY material bucket.
TYPES tt_total_group TYPE HASHED TABLE OF {{p}}if_stock_age=>ty_total
WITH UNIQUE KEY material.
METHODS age_in_days
IMPORTING is_batch TYPE {{p}}if_stock_age=>ty_batch
iv_key_date TYPE dats
RETURNING VALUE(rv_age) TYPE i.
METHODS bucket_of
IMPORTING iv_age TYPE i
RETURNING VALUE(rv_bucket) TYPE {{p}}if_stock_age=>ty_bucket.
ENDCLASS.
CLASS {{p}}stock_age IMPLEMENTATION.
METHOD {{p}}if_stock_age~analyze.
DATA lt_group TYPE tt_age_group.
LOOP AT it_batch INTO DATA(ls_batch).
DATA(lv_age) = age_in_days( is_batch = ls_batch iv_key_date = iv_key_date ).
IF lv_age < 0.
CONTINUE.
ENDIF.
DATA(lv_bucket) = bucket_of( lv_age ).
ASSIGN lt_group[ material = ls_batch-material
bucket = lv_bucket ] TO FIELD-SYMBOL(<ls_group>).
IF sy-subrc <> 0.
INSERT VALUE #( material = ls_batch-material bucket = lv_bucket )
INTO TABLE lt_group.
ASSIGN lt_group[ material = ls_batch-material
bucket = lv_bucket ] TO <ls_group>.
ENDIF.
<ls_group>-quantity = <ls_group>-quantity + ls_batch-quantity.
<ls_group>-batch_count = <ls_group>-batch_count + 1.
ENDLOOP.
rt_age = lt_group.
SORT rt_age BY material bucket.
ENDMETHOD.
METHOD {{p}}if_stock_age~total_by_material.
DATA lt_group TYPE tt_total_group.
LOOP AT it_batch INTO DATA(ls_batch).
IF age_in_days( is_batch = ls_batch iv_key_date = iv_key_date ) < 0.
CONTINUE.
ENDIF.
ASSIGN lt_group[ material = ls_batch-material ] TO FIELD-SYMBOL(<ls_group>).
IF sy-subrc <> 0.
INSERT VALUE #( material = ls_batch-material ) INTO TABLE lt_group.
ASSIGN lt_group[ material = ls_batch-material ] TO <ls_group>.
ENDIF.
<ls_group>-quantity = <ls_group>-quantity + ls_batch-quantity.
<ls_group>-batch_count = <ls_group>-batch_count + 1.
ENDLOOP.
rt_total = lt_group.
SORT rt_total BY material.
ENDMETHOD.
METHOD age_in_days.
IF is_batch-gr_date IS INITIAL OR is_batch-gr_date > iv_key_date.
rv_age = -1.
ELSE.
rv_age = iv_key_date - is_batch-gr_date.
ENDIF.
ENDMETHOD.
METHOD bucket_of.
IF iv_age <= 30.
rv_bucket = {{p}}if_stock_age=>c_bucket-d0_30.
ELSEIF iv_age <= 60.
rv_bucket = {{p}}if_stock_age=>c_bucket-d31_60.
ELSEIF iv_age <= 90.
rv_bucket = {{p}}if_stock_age=>c_bucket-d61_90.
ELSE.
rv_bucket = {{p}}if_stock_age=>c_bucket-d91_plus.
ENDIF.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,80 @@
CLASS ltc_stock_age DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CONSTANTS c_key_date TYPE dats VALUE '20240401'.
DATA mo_cut TYPE REF TO {{p}}if_stock_age.
METHODS setup.
METHODS gr_date_for_age
IMPORTING iv_age TYPE i
RETURNING VALUE(rv_date) TYPE dats.
METHODS empty_input FOR TESTING.
METHODS bucket_01 FOR TESTING.
METHODS bucket_04 FOR TESTING.
METHODS invalid_rows FOR TESTING.
METHODS total_by_material FOR TESTING.
ENDCLASS.
CLASS ltc_stock_age IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}stock_age( ).
ENDMETHOD.
METHOD gr_date_for_age.
rv_date = c_key_date - iv_age.
ENDMETHOD.
METHOD empty_input.
cl_abap_unit_assert=>assert_initial(
act = mo_cut->analyze( it_batch = VALUE {{p}}if_stock_age=>tt_batch( )
iv_key_date = c_key_date ) ).
ENDMETHOD.
METHOD bucket_01.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-1' batch = 'B1' quantity = 10
gr_date = gr_date_for_age( 30 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d0_30 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 10 ).
ENDMETHOD.
METHOD bucket_04.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-1' batch = 'B1' quantity = 10
gr_date = gr_date_for_age( 91 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-bucket
exp = {{p}}if_stock_age=>c_bucket-d91_plus ).
ENDMETHOD.
METHOD invalid_rows.
DATA(lt_result) = mo_cut->analyze(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-1' batch = 'B1' quantity = 10 gr_date = '00000000' ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_initial( act = lt_result ).
ENDMETHOD.
METHOD total_by_material.
DATA(lt_result) = mo_cut->total_by_material(
it_batch = VALUE {{p}}if_stock_age=>tt_batch(
( material = 'MAT-2' batch = 'B1' quantity = 10 gr_date = gr_date_for_age( 1 ) )
( material = 'MAT-1' batch = 'B2' quantity = 5 gr_date = gr_date_for_age( 100 ) ) )
iv_key_date = c_key_date ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-material exp = 'MAT-1' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 1 ]-quantity exp = 5 ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-material exp = 'MAT-2' ).
cl_abap_unit_assert=>assert_equals( act = lt_result[ 2 ]-quantity exp = 10 ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,45 @@
INTERFACE {{p}}if_stock_age PUBLIC.
TYPES:
ty_material TYPE c LENGTH 10,
ty_batch_id TYPE c LENGTH 10,
ty_bucket TYPE c LENGTH 2,
ty_quantity TYPE p LENGTH 13 DECIMALS 3,
BEGIN OF ty_batch,
material TYPE ty_material,
batch TYPE ty_batch_id,
quantity TYPE ty_quantity,
gr_date TYPE dats,
END OF ty_batch,
tt_batch TYPE STANDARD TABLE OF ty_batch WITH EMPTY KEY,
BEGIN OF ty_age_row,
material TYPE ty_material,
bucket TYPE ty_bucket,
quantity TYPE ty_quantity,
batch_count TYPE i,
END OF ty_age_row,
tt_age_row TYPE STANDARD TABLE OF ty_age_row WITH EMPTY KEY,
BEGIN OF ty_total,
material TYPE ty_material,
quantity TYPE ty_quantity,
batch_count TYPE i,
END OF ty_total,
tt_total TYPE STANDARD TABLE OF ty_total WITH EMPTY KEY.
CONSTANTS:
BEGIN OF c_bucket,
d0_30 TYPE ty_bucket VALUE '01',
d31_60 TYPE ty_bucket VALUE '02',
d61_90 TYPE ty_bucket VALUE '03',
d91_plus TYPE ty_bucket VALUE '04',
END OF c_bucket.
METHODS analyze
IMPORTING it_batch TYPE tt_batch
iv_key_date TYPE dats
RETURNING VALUE(rt_age) TYPE tt_age_row.
METHODS total_by_material
IMPORTING it_batch TYPE tt_batch
iv_key_date TYPE dats
RETURNING VALUE(rt_total) TYPE tt_total.
ENDINTERFACE.

View File

@@ -0,0 +1,80 @@
# 1. Goal
Analyse the age of stock batches. A warehouse report calls the analysis before it
prints the stock aging report. The report needs the grouped quantity and the number
of batches for each material and age bucket.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_STOCK_AGE exists in package $TMP. It is active.
- The interface defines these types:
- ty_material: character, length 10.
- ty_batch_id: character, length 10.
- ty_bucket: character, length 2.
- ty_quantity: packed number, length 13, 3 decimals.
- ty_batch: structure with the components material, batch, quantity, gr_date.
- tt_batch: table of ty_batch.
- ty_age_row: structure with the components material, bucket, quantity, batch_count.
- tt_age_row: table of ty_age_row.
- ty_total: structure with the components material, quantity, batch_count.
- tt_total: table of ty_total.
- The interface defines the constant structure C_BUCKET with the components
D0_30 = '01', D31_60 = '02', D61_90 = '03', D91_PLUS = '04'.
- The interface defines two methods:
- analyze: importing it_batch type tt_batch and iv_key_date type dats,
returning rt_age type tt_age_row.
- total_by_material: importing it_batch type tt_batch and iv_key_date type dats,
returning rt_total type tt_total.
# 4. Contract
- Create the class {{P}}STOCK_AGE in package $TMP.
- The class is public, final, and has a public constructor without parameters.
- The class implements {{P}}IF_STOCK_AGE.
- Do not add other public methods.
# 5. Business rules
Both methods use the same validity rules and the same age buckets.
Validity
1. A row of IT_BATCH is valid when its goods receipt date is not initial and is not
after IV_KEY_DATE.
2. A row that is not valid is ignored. It must not change any result.
Age and bucket
3. The age of a batch is the number of days between its goods receipt date and
IV_KEY_DATE.
4. The bucket of a batch is:
- age 0 to 30 days: bucket 01 (C_BUCKET-D0_30)
- age 31 to 60 days: bucket 02 (C_BUCKET-D31_60)
- age 61 to 90 days: bucket 03 (C_BUCKET-D61_90)
- age more than 90 days: bucket 04 (C_BUCKET-D91_PLUS)
Method ANALYZE
5. Group the valid rows by material and bucket.
6. For each group, sum the quantity and count the valid rows.
7. Return one row per group. Sort the result by material ascending and then by
bucket ascending.
8. A group is in the result as soon as one valid row belongs to it, also when the
summed quantity is zero.
9. If no valid row exists, return an empty table.
Method TOTAL_BY_MATERIAL
10. Group the valid rows by material only.
11. For each group, sum the quantity and count the valid rows.
12. Return one row per material. Sort the result by material ascending.
13. If no valid row exists, return an empty table.
Quantity
14. The quantity of a batch can be negative. Add the quantities as they are.
# 6. Constraints
- Release target: SAP_BASIS 8.16 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Method length below 40 statements. No global
variables. No comment that restates the code. Pass large parameters by reference.
- Out of scope: do not change {{P}}IF_STOCK_AGE.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,53 @@
{
"id": "G0004",
"category": "C",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 12
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_STOCK_AGE",
"file": "seed/if_stock_age.intf.abap",
"description": "Stock age contract: row types, bucket constants and the two analysis methods"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}STOCK_AGE",
"implements": "{{P}}IF_STOCK_AGE"
}
],
"out_of_scope": [
"{{P}}IF_STOCK_AGE"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}STOCK_AGE_HIDDEN",
"file": "hidden/stock_age_hidden.clas.abap",
"description": "Hidden tests for the stock age analysis"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}STOCK_AGE",
"file": "reference/stock_age.clas.abap",
"description": "Stock age analysis",
"testclasses_file": "reference/stock_age.testclasses.abap"
}
],
"craft_checks": [
"table_kind",
"access_path",
"method_length",
"no_nested_loops"
]
}

View File

@@ -0,0 +1,29 @@
{
"id": "G0005",
"pool": "eval",
"object_type": "CLAS",
"category": "D",
"attempts": [
{
"stage": "validate",
"oracle": null,
"null": null
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
}
],
"accepted": false
}

View File

@@ -0,0 +1,158 @@
CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES ty_cost TYPE p LENGTH 15 DECIMALS 2.
TYPES ty_currency TYPE c LENGTH 5.
TYPES ty_zone_code TYPE c LENGTH 4.
TYPES ty_weight TYPE p LENGTH 15 DECIMALS 3.
DATA mo_cut TYPE REF TO {{p}}freight_calc.
METHODS setup.
METHODS calculate_result
IMPORTING iv_zone_code TYPE ty_zone_code
iv_weight_kg TYPE ty_weight
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE {{p}}freight_calc=>ty_result.
METHODS assert_cost
IMPORTING iv_cost TYPE ty_cost
iv_currency TYPE ty_currency
is_result TYPE {{p}}freight_calc=>ty_result.
METHODS assert_weight
IMPORTING iv_expected TYPE ty_weight
iv_actual TYPE ty_weight
iv_msg TYPE string.
METHODS standard_cost FOR TESTING.
METHODS express_surcharge FOR TESTING.
METHODS express_no_early_rounding FOR TESTING.
METHODS minimum_charge FOR TESTING.
METHODS rounding FOR TESTING.
METHODS zone_code_normalized FOR TESTING.
METHODS weight_at_limit FOR TESTING.
METHODS unknown_zone FOR TESTING.
METHODS invalid_weight FOR TESTING.
METHODS weight_limit FOR TESTING.
METHODS weight_check_before_zone_lookup FOR TESTING.
ENDCLASS.
CLASS {{p}}t02_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calc( ).
ENDMETHOD.
METHOD calculate_result.
TRY.
rs_result = mo_cut->calculate( iv_zone_code = iv_zone_code
iv_weight_kg = iv_weight_kg
iv_express = iv_express ).
CATCH {{p}}cx_freight_error INTO DATA(lo_error).
cl_abap_unit_assert=>fail( msg = lo_error->get_reason_text( ) ).
ENDTRY.
ENDMETHOD.
METHOD assert_cost.
cl_abap_unit_assert=>assert_equals( exp = iv_cost act = is_result-cost msg = 'cost' ).
cl_abap_unit_assert=>assert_equals( exp = iv_currency act = is_result-currency msg = 'currency' ).
ENDMETHOD.
METHOD assert_weight.
cl_abap_unit_assert=>assert_equals( exp = iv_expected act = iv_actual msg = iv_msg ).
ENDMETHOD.
METHOD standard_cost.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01' iv_weight_kg = '10.000' ).
assert_cost( iv_cost = '17.00' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD express_surcharge.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01'
iv_weight_kg = '10.000'
iv_express = abap_true ).
assert_cost( iv_cost = '25.50' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD express_no_early_rounding.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01'
iv_weight_kg = '10.004'
iv_express = abap_true ).
assert_cost( iv_cost = '25.51' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD minimum_charge.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01' iv_weight_kg = '0.500' ).
assert_cost( iv_cost = '6.00' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD rounding.
DATA(ls_result) = calculate_result( iv_zone_code = 'FR01' iv_weight_kg = '1.234' ).
assert_cost( iv_cost = '12.09' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD zone_code_normalized.
DATA(ls_result) = calculate_result( iv_zone_code = 'de01' iv_weight_kg = '10.000' ).
assert_cost( iv_cost = '17.00' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD weight_at_limit.
DATA(ls_result) = calculate_result( iv_zone_code = 'CN01' iv_weight_kg = '20.000' ).
assert_cost( iv_cost = '103.00' iv_currency = 'CNY' is_result = ls_result ).
ENDMETHOD.
METHOD unknown_zone.
TRY.
mo_cut->calculate( iv_zone_code = 'xx99' iv_weight_kg = '1.000' ).
cl_abap_unit_assert=>fail( msg = 'CX_UNKNOWN_ZONE expected' ).
CATCH {{p}}cx_unknown_zone INTO DATA(lo_unknown).
cl_abap_unit_assert=>assert_equals( exp = 'XX99' act = lo_unknown->mv_zone_code ).
cl_abap_unit_assert=>assert_equals( exp = `Zone XX99 is not known`
act = lo_unknown->get_reason_text( ) ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
METHOD invalid_weight.
TRY.
mo_cut->calculate( iv_zone_code = 'DE01' iv_weight_kg = '0.000' ).
cl_abap_unit_assert=>fail( msg = 'CX_INVALID_WEIGHT expected' ).
CATCH {{p}}cx_invalid_weight INTO DATA(lo_invalid).
assert_weight( iv_expected = '0.000' iv_actual = lo_invalid->mv_weight_kg iv_msg = 'weight' ).
cl_abap_unit_assert=>assert_equals( exp = 'DE01' act = lo_invalid->mv_zone_code ).
cl_abap_unit_assert=>assert_equals( exp = `Weight must be greater than zero`
act = lo_invalid->get_reason_text( ) ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
METHOD weight_limit.
TRY.
mo_cut->calculate( iv_zone_code = 'CN01' iv_weight_kg = '20.001' ).
cl_abap_unit_assert=>fail( msg = 'CX_WEIGHT_LIMIT expected' ).
CATCH {{p}}cx_weight_limit INTO DATA(lo_limit).
assert_weight( iv_expected = '20.001' iv_actual = lo_limit->mv_weight_kg iv_msg = 'weight' ).
cl_abap_unit_assert=>assert_equals( exp = 20 act = lo_limit->mv_max_weight_kg msg = 'max weight' ).
cl_abap_unit_assert=>assert_equals( exp = 'CN01' act = lo_limit->mv_zone_code ).
cl_abap_unit_assert=>assert_equals( exp = `Weight exceeds the zone limit`
act = lo_limit->get_reason_text( ) ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
METHOD weight_check_before_zone_lookup.
TRY.
mo_cut->calculate( iv_zone_code = 'ZZZZ' iv_weight_kg = '0.000' ).
cl_abap_unit_assert=>fail( msg = 'CX_INVALID_WEIGHT expected' ).
CATCH {{p}}cx_invalid_weight INTO DATA(lo_invalid).
cl_abap_unit_assert=>assert_equals( exp = 'ZZZZ' act = lo_invalid->mv_zone_code ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,11 @@
CLASS {{p}}cx_freight_error DEFINITION PUBLIC INHERITING FROM cx_static_check ABSTRACT.
PUBLIC SECTION.
DATA mv_zone_code TYPE c LENGTH 4.
METHODS get_reason_text ABSTRACT
RETURNING VALUE(rv_text) TYPE string.
ENDCLASS.
CLASS {{p}}cx_freight_error IMPLEMENTATION.
ENDCLASS.

View File

@@ -0,0 +1,13 @@
CLASS {{p}}cx_invalid_weight DEFINITION PUBLIC INHERITING FROM {{p}}cx_freight_error FINAL CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_weight_kg TYPE p LENGTH 15 DECIMALS 3.
METHODS get_reason_text REDEFINITION.
ENDCLASS.
CLASS {{p}}cx_invalid_weight IMPLEMENTATION.
METHOD get_reason_text.
rv_text = 'Weight must be greater than zero'.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,11 @@
CLASS {{p}}cx_unknown_zone DEFINITION PUBLIC INHERITING FROM {{p}}cx_freight_error FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS get_reason_text REDEFINITION.
ENDCLASS.
CLASS {{p}}cx_unknown_zone IMPLEMENTATION.
METHOD get_reason_text.
rv_text = |Zone { condense( mv_zone_code ) } is not known|.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,14 @@
CLASS {{p}}cx_weight_limit DEFINITION PUBLIC INHERITING FROM {{p}}cx_freight_error FINAL CREATE PUBLIC.
PUBLIC SECTION.
DATA mv_weight_kg TYPE p LENGTH 15 DECIMALS 3.
DATA mv_max_weight_kg TYPE i.
METHODS get_reason_text REDEFINITION.
ENDCLASS.
CLASS {{p}}cx_weight_limit IMPLEMENTATION.
METHOD get_reason_text.
rv_text = 'Weight exceeds the zone limit'.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,73 @@
CLASS {{p}}freight_calc DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES ty_cost TYPE p LENGTH 15 DECIMALS 2.
TYPES ty_weight TYPE p LENGTH 15 DECIMALS 3.
TYPES: BEGIN OF ty_result,
cost TYPE ty_cost,
currency TYPE c LENGTH 5,
END OF ty_result.
METHODS calculate
IMPORTING iv_zone_code TYPE c LENGTH 4
iv_weight_kg TYPE ty_weight
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE ty_result
RAISING {{p}}cx_freight_error.
PRIVATE SECTION.
CONSTANTS c_express_factor TYPE p LENGTH 5 DECIMALS 2 VALUE '1.50'.
CONSTANTS c_minimum_charge TYPE ty_cost VALUE '6.00'.
METHODS find_zone
IMPORTING iv_zone_code TYPE c LENGTH 4
RETURNING VALUE(rs_zone) TYPE {{p}}zone_master=>ty_zone
RAISING {{p}}cx_freight_error.
ENDCLASS.
CLASS {{p}}freight_calc IMPLEMENTATION.
METHOD calculate.
DATA lv_zone_code TYPE c LENGTH 4.
lv_zone_code = to_upper( iv_zone_code ).
IF iv_weight_kg <= 0.
DATA(lo_invalid) = NEW {{p}}cx_invalid_weight( ).
lo_invalid->mv_zone_code = lv_zone_code.
lo_invalid->mv_weight_kg = iv_weight_kg.
RAISE EXCEPTION lo_invalid.
ENDIF.
DATA(ls_zone) = find_zone( lv_zone_code ).
IF iv_weight_kg > ls_zone-max_weight_kg.
DATA(lo_limit) = NEW {{p}}cx_weight_limit( ).
lo_limit->mv_zone_code = lv_zone_code.
lo_limit->mv_weight_kg = iv_weight_kg.
lo_limit->mv_max_weight_kg = ls_zone-max_weight_kg.
RAISE EXCEPTION lo_limit.
ENDIF.
DATA lv_cost TYPE p LENGTH 15 DECIMALS 5.
lv_cost = ls_zone-base_fee + iv_weight_kg * ls_zone-price_per_kg.
IF iv_express = abap_true.
lv_cost = lv_cost * c_express_factor.
ENDIF.
lv_cost = round( val = lv_cost dec = 2 ).
IF lv_cost < c_minimum_charge.
lv_cost = c_minimum_charge.
ENDIF.
rs_result-cost = lv_cost.
rs_result-currency = ls_zone-currency.
ENDMETHOD.
METHOD find_zone.
DATA(lt_zone) = {{p}}zone_master=>get_all( ).
READ TABLE lt_zone INTO rs_zone WITH KEY zone_code = iv_zone_code.
IF sy-subrc <> 0.
DATA(lo_unknown) = NEW {{p}}cx_unknown_zone( ).
lo_unknown->mv_zone_code = iv_zone_code.
RAISE EXCEPTION lo_unknown.
ENDIF.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,98 @@
CLASS ltc_freight_calc DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES ty_cost TYPE p LENGTH 15 DECIMALS 2.
TYPES ty_currency TYPE c LENGTH 5.
TYPES ty_zone_code TYPE c LENGTH 4.
TYPES ty_weight TYPE p LENGTH 15 DECIMALS 3.
DATA mo_cut TYPE REF TO {{p}}freight_calc.
METHODS setup.
METHODS assert_cost
IMPORTING iv_cost TYPE ty_cost
iv_currency TYPE ty_currency
is_result TYPE {{p}}freight_calc=>ty_result.
METHODS calculate_result
IMPORTING iv_zone_code TYPE ty_zone_code
iv_weight_kg TYPE ty_weight
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE {{p}}freight_calc=>ty_result.
METHODS standard_cost FOR TESTING.
METHODS express_cost FOR TESTING.
METHODS minimum_charge FOR TESTING.
METHODS unknown_zone FOR TESTING.
METHODS invalid_weight FOR TESTING.
METHODS weight_limit FOR TESTING.
ENDCLASS.
CLASS ltc_freight_calc IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calc( ).
ENDMETHOD.
METHOD assert_cost.
cl_abap_unit_assert=>assert_equals( exp = iv_cost act = is_result-cost ).
cl_abap_unit_assert=>assert_equals( exp = iv_currency act = is_result-currency ).
ENDMETHOD.
METHOD calculate_result.
TRY.
rs_result = mo_cut->calculate( iv_zone_code = iv_zone_code
iv_weight_kg = iv_weight_kg
iv_express = iv_express ).
CATCH {{p}}cx_freight_error INTO DATA(lo_error).
cl_abap_unit_assert=>fail( msg = lo_error->get_reason_text( ) ).
ENDTRY.
ENDMETHOD.
METHOD standard_cost.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01' iv_weight_kg = '10.000' ).
assert_cost( iv_cost = '17.00' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD express_cost.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01'
iv_weight_kg = '10.000'
iv_express = abap_true ).
assert_cost( iv_cost = '25.50' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD minimum_charge.
DATA(ls_result) = calculate_result( iv_zone_code = 'DE01' iv_weight_kg = '0.500' ).
assert_cost( iv_cost = '6.00' iv_currency = 'EUR' is_result = ls_result ).
ENDMETHOD.
METHOD unknown_zone.
TRY.
mo_cut->calculate( iv_zone_code = 'XX99' iv_weight_kg = '1.000' ).
cl_abap_unit_assert=>fail( msg = 'CX_UNKNOWN_ZONE expected' ).
CATCH {{p}}cx_unknown_zone INTO DATA(lo_unknown).
cl_abap_unit_assert=>assert_equals( exp = 'XX99' act = lo_unknown->mv_zone_code ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
METHOD invalid_weight.
TRY.
mo_cut->calculate( iv_zone_code = 'DE01' iv_weight_kg = '0.000' ).
cl_abap_unit_assert=>fail( msg = 'CX_INVALID_WEIGHT expected' ).
CATCH {{p}}cx_invalid_weight INTO DATA(lo_invalid).
cl_abap_unit_assert=>assert_equals( exp = 'DE01' act = lo_invalid->mv_zone_code ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
METHOD weight_limit.
TRY.
mo_cut->calculate( iv_zone_code = 'CN01' iv_weight_kg = '20.001' ).
cl_abap_unit_assert=>fail( msg = 'CX_WEIGHT_LIMIT expected' ).
CATCH {{p}}cx_weight_limit INTO DATA(lo_limit).
cl_abap_unit_assert=>assert_equals( exp = 'CN01' act = lo_limit->mv_zone_code ).
CATCH {{p}}cx_freight_error INTO DATA(lo_other).
cl_abap_unit_assert=>fail( msg = |Wrong exception class: { lo_other->get_reason_text( ) }| ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,18 @@
CLASS {{p}}seed_data DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
ENDCLASS.
CLASS {{p}}seed_data IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DELETE FROM {{p}}zones.
INSERT {{p}}zones FROM TABLE @( VALUE #(
( zone_code = 'DE01' country = 'DE' base_fee = '5.00' price_per_kg = '1.20' max_weight_kg = 31 currency = 'EUR' )
( zone_code = 'DE02' country = 'DE' base_fee = '8.00' price_per_kg = '1.80' max_weight_kg = 31 currency = 'EUR' )
( zone_code = 'FR01' country = 'FR' base_fee = '9.50' price_per_kg = '2.10' max_weight_kg = 30 currency = 'EUR' )
( zone_code = 'US01' country = 'US' base_fee = '14.00' price_per_kg = '3.40' max_weight_kg = 25 currency = 'USD' )
( zone_code = 'CN01' country = 'CN' base_fee = '18.00' price_per_kg = '4.25' max_weight_kg = 20 currency = 'CNY' ) ) ).
out->write( |Zones seeded| ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,27 @@
CLASS {{p}}zone_master DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_zone,
zone_code TYPE c LENGTH 4,
country TYPE c LENGTH 2,
base_fee TYPE p LENGTH 15 DECIMALS 2,
price_per_kg TYPE p LENGTH 15 DECIMALS 2,
max_weight_kg TYPE i,
currency TYPE c LENGTH 5,
END OF ty_zone,
tt_zone TYPE SORTED TABLE OF ty_zone WITH UNIQUE KEY zone_code.
CLASS-METHODS get_all
RETURNING VALUE(rt_zone) TYPE tt_zone.
ENDCLASS.
CLASS {{p}}zone_master IMPLEMENTATION.
METHOD get_all.
rt_zone = VALUE #(
( zone_code = 'DE01' country = 'DE' base_fee = '5.00' price_per_kg = '1.20' max_weight_kg = 31 currency = 'EUR' )
( zone_code = 'DE02' country = 'DE' base_fee = '8.00' price_per_kg = '1.80' max_weight_kg = 31 currency = 'EUR' )
( zone_code = 'FR01' country = 'FR' base_fee = '9.50' price_per_kg = '2.10' max_weight_kg = 30 currency = 'EUR' )
( zone_code = 'US01' country = 'US' base_fee = '14.00' price_per_kg = '3.40' max_weight_kg = 25 currency = 'USD' )
( zone_code = 'CN01' country = 'CN' base_fee = '18.00' price_per_kg = '4.25' max_weight_kg = 20 currency = 'CNY' ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,10 @@
@EndUserText.label : 'Zone tariff'
define table {{p}}zones {
key mandt : abap.clnt not null;
key zone_code : abap.char(4) not null;
country : abap.char(2) not null;
base_fee : abap.dec(15,2) not null;
price_per_kg : abap.dec(15,2) not null;
max_weight_kg : abap.int4 not null;
currency : abap.char(5) not null;
}

View File

@@ -0,0 +1,103 @@
# 1. Goal
A parcel service calculates the freight cost of a parcel from a zone code and a
weight. Each rejected input must be reported as its own class-based exception, so
that the calling program can react to the exact reason.
# 2. Open questions
None.
# 3. Context
- The class {{P}}ZONE_MASTER exists in package $TMP and is active. It provides the
zone master data. Do not change it.
- {{P}}ZONE_MASTER has these public components:
- TYPES TY_ZONE with the components ZONE_CODE (c LENGTH 4), COUNTRY (c LENGTH 2),
BASE_FEE (p LENGTH 15 DECIMALS 2), PRICE_PER_KG (p LENGTH 15 DECIMALS 2),
MAX_WEIGHT_KG (i), CURRENCY (c LENGTH 5).
- TYPES TT_ZONE, a sorted table of TY_ZONE with the unique key ZONE_CODE.
- CLASS-METHOD GET_ALL returning the table TT_ZONE.
- The zone master data contains these entries:
| ZONE_CODE | COUNTRY | BASE_FEE | PRICE_PER_KG | MAX_WEIGHT_KG | CURRENCY |
| --------- | ------- | -------- | ------------ | ------------- | -------- |
| DE01 | DE | 5.00 | 1.20 | 31 | EUR |
| DE02 | DE | 8.00 | 1.80 | 31 | EUR |
| FR01 | FR | 9.50 | 2.10 | 30 | EUR |
| US01 | US | 14.00 | 3.40 | 25 | USD |
| CN01 | CN | 18.00 | 4.25 | 20 | CNY |
# 4. Contract
Create these classes in package $TMP.
{{P}}CX_FREIGHT_ERROR
- public, abstract, inherits from CX_STATIC_CHECK.
- public attribute MV_ZONE_CODE type c LENGTH 4.
- public method GET_REASON_TEXT returning VALUE(rv_text) type string.
{{P}}CX_UNKNOWN_ZONE
- public, inherits from {{P}}CX_FREIGHT_ERROR.
{{P}}CX_INVALID_WEIGHT
- public, inherits from {{P}}CX_FREIGHT_ERROR.
- public attribute MV_WEIGHT_KG type p LENGTH 15 DECIMALS 3.
{{P}}CX_WEIGHT_LIMIT
- public, inherits from {{P}}CX_FREIGHT_ERROR.
- public attribute MV_WEIGHT_KG type p LENGTH 15 DECIMALS 3.
- public attribute MV_MAX_WEIGHT_KG type i.
GET_REASON_TEXT returns the reason text of the exception. Each of the three
concrete exception classes returns its own text, see the business rules.
{{P}}FREIGHT_CALC
- public, final, create public, constructor without parameters.
- public type TY_COST (p LENGTH 15 DECIMALS 2).
- public type TY_WEIGHT (p LENGTH 15 DECIMALS 3).
- public type TY_RESULT with the components COST (type TY_COST) and CURRENCY
(c LENGTH 5).
- public method CALCULATE:
IMPORTING iv_zone_code TYPE c LENGTH 4
iv_weight_kg TYPE ty_weight
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE ty_result
RAISING {{p}}cx_freight_error
- no other public methods.
Use the named types of this contract in the method interfaces. Do not write the
additions LENGTH or DECIMALS in a method interface.
# 5. Business rules
CALCULATE applies these rules in this sequence.
1. Convert the zone code to upper case. Use this value in all further steps and in
the attribute MV_ZONE_CODE of every exception.
2. If the weight is less than or equal to zero, raise {{P}}CX_INVALID_WEIGHT. Set
MV_ZONE_CODE to the zone code and MV_WEIGHT_KG to the given weight.
3. Read the zone master data with {{P}}ZONE_MASTER=>GET_ALL and find the entry
whose ZONE_CODE equals the zone code. If there is no such entry, raise
{{P}}CX_UNKNOWN_ZONE and set MV_ZONE_CODE to the zone code.
4. If the weight is greater than MAX_WEIGHT_KG, raise {{P}}CX_WEIGHT_LIMIT. Set
MV_ZONE_CODE to the zone code, MV_WEIGHT_KG to the given weight and
MV_MAX_WEIGHT_KG to MAX_WEIGHT_KG.
5. Calculate the cost as BASE_FEE + weight x PRICE_PER_KG.
6. If IV_EXPRESS is ABAP_TRUE, multiply the cost by 1.5.
7. Keep the full precision of the cost until this step. Do not round the cost
before this step. Round the cost to 2 decimal places with commercial rounding.
8. If the rounded cost is less than 6.00, use 6.00.
9. Return the cost in COST and the CURRENCY of the zone in CURRENCY.
GET_REASON_TEXT returns:
- in {{P}}CX_UNKNOWN_ZONE: 'Zone ', then the zone code, then ' is not known'.
Example: 'Zone XX99 is not known'.
- in {{P}}CX_INVALID_WEIGHT: 'Weight must be greater than zero'.
- in {{P}}CX_WEIGHT_LIMIT: 'Weight exceeds the zone limit'.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Methods below 40 statements. No global variables.
- Out of scope: do not change {{P}}ZONE_MASTER.
# 7. Acceptance
- All classes are active and have no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for {{P}}FREIGHT_CALC.

View File

@@ -0,0 +1,90 @@
{
"id": "G0005",
"category": "D",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 20
},
"seed": [
{
"type": "CLAS",
"name": "{{P}}ZONE_MASTER",
"file": "seed/zone_master.clas.abap",
"description": "Zone master data provider"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}CX_FREIGHT_ERROR"
},
{
"type": "CLAS",
"name": "{{P}}CX_UNKNOWN_ZONE"
},
{
"type": "CLAS",
"name": "{{P}}CX_INVALID_WEIGHT"
},
{
"type": "CLAS",
"name": "{{P}}CX_WEIGHT_LIMIT"
},
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALC"
}
],
"out_of_scope": [
"{{P}}ZONE_MASTER"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}CX_FREIGHT_ERROR",
"file": "reference/cx_freight_error.clas.abap",
"description": "Abstract base exception"
},
{
"type": "CLAS",
"name": "{{P}}CX_UNKNOWN_ZONE",
"file": "reference/cx_unknown_zone.clas.abap",
"description": "Unknown zone exception"
},
{
"type": "CLAS",
"name": "{{P}}CX_INVALID_WEIGHT",
"file": "reference/cx_invalid_weight.clas.abap",
"description": "Invalid weight exception"
},
{
"type": "CLAS",
"name": "{{P}}CX_WEIGHT_LIMIT",
"file": "reference/cx_weight_limit.clas.abap",
"description": "Weight limit exception"
},
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALC",
"file": "reference/freight_calc.clas.abap",
"description": "Freight cost calculator",
"testclasses_file": "reference/freight_calc.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"exception_handling"
]
}

View File

@@ -0,0 +1,30 @@
{
"id": "G0006",
"pool": "eval",
"object_type": "CLAS",
"category": "E",
"attempts": [
{
"stage": "validate",
"oracle": null,
"null": null
},
{
"stage": "validate",
"oracle": null,
"null": null
},
{
"stage": "bundle",
"errors": [
"invalid JSON: Expecting value: line 1 column 1 (char 0)"
]
},
{
"stage": "validate",
"oracle": null,
"null": null
}
],
"accepted": false
}

View File

@@ -0,0 +1,154 @@
CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}freight_calculator.
METHODS setup.
METHODS assert_calculation
IMPORTING iv_weight_kg TYPE p LENGTH 8 DECIMALS 2
iv_zone TYPE string
iv_express TYPE abap_bool DEFAULT abap_false
is_expected TYPE {{p}}freight_calculator=>ty_result.
METHODS standard_parcel FOR TESTING.
METHODS express_parcel FOR TESTING.
METHODS zone_normalization FOR TESTING.
METHODS rounding_half_up FOR TESTING.
METHODS zero_weight FOR TESTING.
METHODS negative_weight FOR TESTING.
METHODS unknown_zone FOR TESTING.
METHODS weight_error_wins FOR TESTING.
METHODS supported_zones FOR TESTING.
METHODS unsupported_zones FOR TESTING.
METHODS all_rates FOR TESTING.
ENDCLASS.
CLASS {{p}}t02_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calculator( ).
ENDMETHOD.
METHOD assert_calculation.
cl_abap_unit_assert=>assert_equals(
exp = is_expected
act = mo_cut->calculate( iv_weight_kg = iv_weight_kg
iv_zone = iv_zone
iv_express = iv_express )
msg = |zone { iv_zone }| ).
ENDMETHOD.
METHOD standard_parcel.
assert_calculation(
iv_weight_kg = '10.00'
iv_zone = `DE`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok
base = '45.00'
surcharge = '0.00'
total = '45.00' ) ).
ENDMETHOD.
METHOD express_parcel.
assert_calculation(
iv_weight_kg = '10.00'
iv_zone = `DE`
iv_express = abap_true
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok
base = '45.00'
surcharge = '11.25'
total = '56.25' ) ).
ENDMETHOD.
METHOD zone_normalization.
assert_calculation(
iv_weight_kg = '5.00'
iv_zone = ` ch `
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok
base = '39.00'
total = '39.00' ) ).
assert_calculation(
iv_weight_kg = '5.00'
iv_zone = `de`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok
base = '22.50'
total = '22.50' ) ).
ENDMETHOD.
METHOD rounding_half_up.
assert_calculation(
iv_weight_kg = '3.33'
iv_zone = `DE`
iv_express = abap_true
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok
base = '14.99'
surcharge = '3.75'
total = '18.74' ) ).
ENDMETHOD.
METHOD zero_weight.
assert_calculation(
iv_weight_kg = '0.00'
iv_zone = `DE`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-weight ) ).
ENDMETHOD.
METHOD negative_weight.
assert_calculation(
iv_weight_kg = '-2.50'
iv_zone = `DE`
iv_express = abap_true
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-weight ) ).
ENDMETHOD.
METHOD unknown_zone.
assert_calculation(
iv_weight_kg = '5.00'
iv_zone = `XX`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-zone ) ).
assert_calculation(
iv_weight_kg = '5.00'
iv_zone = `DEU`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-zone ) ).
ENDMETHOD.
METHOD weight_error_wins.
assert_calculation(
iv_weight_kg = '0.00'
iv_zone = `XX`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-weight ) ).
ENDMETHOD.
METHOD supported_zones.
cl_abap_unit_assert=>assert_equals( exp = abap_true act = mo_cut->is_zone_supported( `de` ) msg = `de` ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = mo_cut->is_zone_supported( ` DE ` ) msg = ` DE ` ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = mo_cut->is_zone_supported( `at` ) msg = `at` ).
cl_abap_unit_assert=>assert_equals( exp = abap_true act = mo_cut->is_zone_supported( `es` ) msg = `es` ).
ENDMETHOD.
METHOD unsupported_zones.
DATA lv_empty TYPE string.
cl_abap_unit_assert=>assert_equals( exp = abap_false act = mo_cut->is_zone_supported( `XX` ) msg = `XX` ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = mo_cut->is_zone_supported( lv_empty ) msg = `empty` ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = mo_cut->is_zone_supported( ` ` ) msg = `blanks` ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = mo_cut->is_zone_supported( `DEU` ) msg = `DEU` ).
cl_abap_unit_assert=>assert_equals( exp = abap_false act = mo_cut->is_zone_supported( `DE1` ) msg = `DE1` ).
ENDMETHOD.
METHOD all_rates.
assert_calculation( iv_weight_kg = '1.00' iv_zone = `DE`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '4.50' total = '4.50' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `AT`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '5.20' total = '5.20' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `CH`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '7.80' total = '7.80' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `FR`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '6.40' total = '6.40' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `NL`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '4.90' total = '4.90' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `PL`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '5.10' total = '5.10' ) ).
assert_calculation( iv_weight_kg = '1.00' iv_zone = `ES`
is_expected = VALUE #( status = {{p}}freight_calculator=>c_status-ok base = '6.90' total = '6.90' ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,130 @@
CLASS {{p}}freight_calculator DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_result,
status TYPE c LENGTH 10,
base TYPE p LENGTH 9 DECIMALS 2,
surcharge TYPE p LENGTH 9 DECIMALS 2,
total TYPE p LENGTH 9 DECIMALS 2,
END OF ty_result.
CONSTANTS:
BEGIN OF c_status,
ok TYPE c LENGTH 10 VALUE 'OK',
weight TYPE c LENGTH 10 VALUE 'WEIGHT',
zone TYPE c LENGTH 10 VALUE 'ZONE',
END OF c_status.
METHODS calculate
IMPORTING
iv_weight_kg TYPE p LENGTH 8 DECIMALS 2
iv_zone TYPE string
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING
VALUE(rs_result) TYPE ty_result.
METHODS is_zone_supported
IMPORTING
iv_zone TYPE string
RETURNING
VALUE(rv_supported) TYPE abap_bool.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_rate,
zone TYPE c LENGTH 2,
rate TYPE p LENGTH 6 DECIMALS 2,
END OF ty_rate,
tt_rate TYPE HASHED TABLE OF ty_rate WITH UNIQUE KEY zone.
CONSTANTS c_express_share TYPE p LENGTH 5 DECIMALS 2 VALUE '0.25'.
METHODS normalize_zone
IMPORTING iv_zone TYPE string
RETURNING VALUE(rv_zone) TYPE string.
METHODS rates
RETURNING VALUE(rt_rate) TYPE tt_rate.
METHODS rate_for_zone
IMPORTING iv_zone TYPE string
RETURNING VALUE(rv_rate) TYPE p LENGTH 6 DECIMALS 2.
METHODS empty_result
IMPORTING iv_status TYPE c LENGTH 10
RETURNING VALUE(rs_result) TYPE ty_result.
METHODS base_amount
IMPORTING iv_rate TYPE p LENGTH 6 DECIMALS 2
iv_weight_kg TYPE p LENGTH 8 DECIMALS 2
RETURNING VALUE(rv_base) TYPE p LENGTH 9 DECIMALS 2.
METHODS surcharge_amount
IMPORTING iv_base TYPE p LENGTH 9 DECIMALS 2
iv_express TYPE abap_bool
RETURNING VALUE(rv_surcharge) TYPE p LENGTH 9 DECIMALS 2.
ENDCLASS.
CLASS {{p}}freight_calculator IMPLEMENTATION.
METHOD calculate.
DATA(lv_zone) = normalize_zone( iv_zone ).
IF iv_weight_kg <= 0.
rs_result = empty_result( c_status-weight ).
RETURN.
ENDIF.
IF is_zone_supported( lv_zone ) = abap_false.
rs_result = empty_result( c_status-zone ).
RETURN.
ENDIF.
rs_result-base = base_amount( iv_rate = rate_for_zone( lv_zone )
iv_weight_kg = iv_weight_kg ).
rs_result-surcharge = surcharge_amount( iv_base = rs_result-base
iv_express = iv_express ).
rs_result-total = rs_result-base + rs_result-surcharge.
rs_result-status = c_status-ok.
ENDMETHOD.
METHOD is_zone_supported.
DATA(lv_zone) = normalize_zone( iv_zone ).
DATA(lt_rate) = rates( ).
rv_supported = xsdbool( line_exists( lt_rate[ zone = lv_zone ] ) ).
ENDMETHOD.
METHOD normalize_zone.
rv_zone = to_upper( replace( val = iv_zone sub = ` ` with = `` occ = 0 ) ).
ENDMETHOD.
METHOD rates.
rt_rate = VALUE #( ( zone = 'DE' rate = '4.50' )
( zone = 'AT' rate = '5.20' )
( zone = 'CH' rate = '7.80' )
( zone = 'FR' rate = '6.40' )
( zone = 'NL' rate = '4.90' )
( zone = 'PL' rate = '5.10' )
( zone = 'ES' rate = '6.90' ) ).
ENDMETHOD.
METHOD rate_for_zone.
DATA(lt_rate) = rates( ).
READ TABLE lt_rate WITH TABLE KEY zone = iv_zone INTO DATA(ls_rate).
rv_rate = ls_rate-rate.
ENDMETHOD.
METHOD empty_result.
rs_result = VALUE #( status = iv_status ).
ENDMETHOD.
METHOD base_amount.
DATA lv_amount TYPE p LENGTH 13 DECIMALS 4.
lv_amount = iv_rate * iv_weight_kg.
rv_base = round( val = lv_amount dec = 2 ).
ENDMETHOD.
METHOD surcharge_amount.
IF iv_express = abap_false.
RETURN.
ENDIF.
DATA lv_amount TYPE p LENGTH 13 DECIMALS 4.
lv_amount = iv_base * c_express_share.
rv_surcharge = round( val = lv_amount dec = 2 ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,56 @@
CLASS ltc_freight_calculator DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}freight_calculator.
METHODS setup.
METHODS base_parcel FOR TESTING.
METHODS express_parcel FOR TESTING.
METHODS zone_is_normalized FOR TESTING.
METHODS weight_error FOR TESTING.
METHODS unknown_zone FOR TESTING.
METHODS zone_support FOR TESTING.
ENDCLASS.
CLASS ltc_freight_calculator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}freight_calculator( ).
ENDMETHOD.
METHOD base_parcel.
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'OK' base = '45.00' total = '45.00' )
act = mo_cut->calculate( iv_weight_kg = '10.00' iv_zone = `DE` ) ).
ENDMETHOD.
METHOD express_parcel.
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'OK'
base = '45.00'
surcharge = '11.25'
total = '56.25' )
act = mo_cut->calculate( iv_weight_kg = '10.00' iv_zone = `DE` iv_express = abap_true ) ).
ENDMETHOD.
METHOD zone_is_normalized.
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'OK' base = '39.00' total = '39.00' )
act = mo_cut->calculate( iv_weight_kg = '5.00' iv_zone = ` ch ` ) ).
ENDMETHOD.
METHOD weight_error.
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'WEIGHT' )
act = mo_cut->calculate( iv_weight_kg = '0.00' iv_zone = `DE` ) ).
ENDMETHOD.
METHOD unknown_zone.
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'ZONE' )
act = mo_cut->calculate( iv_weight_kg = '5.00' iv_zone = `XX` ) ).
ENDMETHOD.
METHOD zone_support.
cl_abap_unit_assert=>assert_true( mo_cut->is_zone_supported( `de` ) ).
cl_abap_unit_assert=>assert_false( mo_cut->is_zone_supported( `XX` ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,106 @@
CLASS {{p}}freight_calculator DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_result,
status TYPE c LENGTH 10,
base TYPE p LENGTH 9 DECIMALS 2,
surcharge TYPE p LENGTH 9 DECIMALS 2,
total TYPE p LENGTH 9 DECIMALS 2,
END OF ty_result.
CONSTANTS:
BEGIN OF c_status,
ok TYPE c LENGTH 10 VALUE 'OK',
weight TYPE c LENGTH 10 VALUE 'WEIGHT',
zone TYPE c LENGTH 10 VALUE 'ZONE',
END OF c_status.
METHODS calculate
IMPORTING
iv_weight_kg TYPE p LENGTH 8 DECIMALS 2
iv_zone TYPE string
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING
VALUE(rs_result) TYPE ty_result.
METHODS is_zone_supported
IMPORTING
iv_zone TYPE string
RETURNING
VALUE(rv_supported) TYPE abap_bool.
PRIVATE SECTION.
DATA mv_last_zone TYPE string.
CONSTANTS c_express_share TYPE p LENGTH 5 DECIMALS 2 VALUE '0.25'.
ENDCLASS.
CLASS {{p}}freight_calculator IMPLEMENTATION.
METHOD calculate.
DATA lv_zone TYPE string.
DATA lv_rate TYPE p LENGTH 6 DECIMALS 2.
DATA lv_amount TYPE p LENGTH 13 DECIMALS 4.
DATA lv_express TYPE abap_bool.
lv_zone = to_upper( replace( val = iv_zone sub = ` ` with = `` occ = 0 ) ).
mv_last_zone = lv_zone.
IF iv_weight_kg <= 0.
rs_result-status = 'WEIGHT'.
rs_result-base = 0.
rs_result-surcharge = 0.
rs_result-total = 0.
RETURN.
ENDIF.
* IF lv_zone = 'BE'.
* lv_rate = '5.60'.
* ENDIF.
IF lv_zone = 'DE'.
lv_rate = '4.50'.
ELSEIF lv_zone = 'AT'.
lv_rate = '5.20'.
ELSEIF lv_zone = 'CH'.
lv_rate = '7.80'.
ELSEIF lv_zone = 'FR'.
lv_rate = '6.40'.
ELSEIF lv_zone = 'NL'.
lv_rate = '4.90'.
ELSEIF lv_zone = 'PL'.
lv_rate = '5.10'.
ELSEIF lv_zone = 'ES'.
lv_rate = '6.90'.
ELSE.
rs_result-status = 'ZONE'.
rs_result-base = 0.
rs_result-surcharge = 0.
rs_result-total = 0.
RETURN.
ENDIF.
lv_amount = lv_rate * iv_weight_kg.
rs_result-base = round( val = lv_amount dec = 2 ).
lv_express = iv_express.
IF lv_express = abap_true.
lv_amount = rs_result-base * c_express_share.
rs_result-surcharge = round( val = lv_amount dec = 2 ).
ELSE.
rs_result-surcharge = 0.
ENDIF.
rs_result-total = rs_result-base + rs_result-surcharge.
rs_result-status = 'OK'.
ENDMETHOD.
METHOD is_zone_supported.
DATA lv_zone TYPE string.
lv_zone = to_upper( replace( val = iv_zone sub = ` ` with = `` occ = 0 ) ).
IF lv_zone = 'DE' OR lv_zone = 'AT' OR lv_zone = 'CH' OR lv_zone = 'FR'
OR lv_zone = 'NL' OR lv_zone = 'PL' OR lv_zone = 'ES'.
rv_supported = abap_true.
ELSE.
rv_supported = abap_false.
ENDIF.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,25 @@
CLASS ltc_legacy_freight DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS base_parcel FOR TESTING.
METHODS express_parcel FOR TESTING.
ENDCLASS.
CLASS ltc_legacy_freight IMPLEMENTATION.
METHOD base_parcel.
DATA(lo_cut) = NEW {{p}}freight_calculator( ).
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'OK' base = '45.00' total = '45.00' )
act = lo_cut->calculate( iv_weight_kg = '10.00' iv_zone = `DE` ) ).
ENDMETHOD.
METHOD express_parcel.
DATA(lo_cut) = NEW {{p}}freight_calculator( ).
cl_abap_unit_assert=>assert_equals(
exp = VALUE {{p}}freight_calculator=>ty_result( status = 'OK'
base = '45.00'
surcharge = '11.25'
total = '56.25' )
act = lo_cut->calculate( iv_weight_kg = '10.00' iv_zone = `DE` iv_express = abap_true ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,67 @@
# 1. Goal
Refactor the class {{P}}FREIGHT_CALCULATOR. The class calculates the freight cost of a
parcel. The calculation is correct, but the code is hard to read and hard to maintain.
Make the code clean. The behavior and the public API must not change.
# 2. Open questions
None.
# 3. Context
- The class {{P}}FREIGHT_CALCULATOR exists in package $TMP. It is active.
- The class contains legacy code: one long method, nested IF blocks, hard-coded
numbers, a private attribute that is written but never read, and commented-out code.
- A logistics program calls the class. That program must continue to work without a
change. Therefore the behavior of the class must stay the same.
# 4. Contract
The public part of the class must stay exactly as it is:
- The class {{P}}FREIGHT_CALCULATOR is public, final, and has a public constructor
without parameters.
- Public type TY_RESULT with these components: STATUS TYPE c LENGTH 10,
BASE TYPE p LENGTH 9 DECIMALS 2, SURCHARGE TYPE p LENGTH 9 DECIMALS 2,
TOTAL TYPE p LENGTH 9 DECIMALS 2.
- Public constant structure C_STATUS with the components OK, WEIGHT and ZONE. Each
component has the type c LENGTH 10. The values are 'OK', 'WEIGHT' and 'ZONE'.
- Public method CALCULATE:
IMPORTING iv_weight_kg TYPE p LENGTH 8 DECIMALS 2
iv_zone TYPE string
iv_express TYPE abap_bool DEFAULT abap_false
RETURNING VALUE(rs_result) TYPE ty_result.
- Public method IS_ZONE_SUPPORTED:
IMPORTING iv_zone TYPE string
RETURNING VALUE(rv_supported) TYPE abap_bool.
- Do not add other public components or public methods.
# 5. Business rules
The rules below describe the behavior of the class. The behavior must not change.
1. Normalize the zone: remove all blanks and convert all letters to upper case. A
zone is known only if the normalized zone is exactly equal to a zone code. Do not
shorten the zone and do not extend the zone.
2. Check the weight first. If the weight is not greater than zero, return the status
WEIGHT with BASE, SURCHARGE and TOTAL equal to zero. This check comes before the
zone check.
3. If the normalized zone is not known, return the status ZONE with BASE, SURCHARGE
and TOTAL equal to zero.
4. The rate per kilogram depends on the zone: DE 4.50, AT 5.20, CH 7.80, FR 6.40,
NL 4.90, PL 5.10, ES 6.90.
5. BASE = rate x weight. Round BASE to 2 decimal places. Round half away from zero.
6. If iv_express is abap_true, SURCHARGE = BASE x 25 / 100, rounded to 2 decimal
places. Else SURCHARGE is zero.
7. TOTAL = BASE + SURCHARGE.
8. Return the status OK.
9. IS_ZONE_SUPPORTED returns abap_true if the normalized zone is known, else
abap_false. A blank zone is not known.
# 6. Constraints
- Release target: SAP_BASIS 816 (ABAP Platform 2025).
- Coding standards: Clean ABAP. Every method has less than 40 statements. No global
variables. No IF block nested deeper than 2 levels. No hard-coded rate in a
statement: use a constant or a table. Remove the private attribute that is never
read. Remove the commented-out code. No comment that restates the code.
- Out of scope: do not change the public API, do not change the behavior, do not
create new global objects, do not change the class name.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests as local test classes in the class.

View File

@@ -0,0 +1,54 @@
{
"id": "G0006",
"category": "E",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 60,
"max_activations": 15
},
"seed": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALCULATOR",
"file": "seed/freight_calculator.clas.abap",
"description": "Legacy freight calculator. Correct behavior, poor structure: one long method, nested IF blocks, hard-coded rates, a private attribute that is written but never read, commented-out code."
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALCULATOR"
}
],
"out_of_scope": [
"New global objects",
"Changes to the public API of {{P}}FREIGHT_CALCULATOR",
"Changes to the result of the calculation"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests for the refactored freight calculator"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALCULATOR",
"file": "reference/freight_calculator.clas.abap",
"description": "Refactored freight calculator with extracted methods and a rate table",
"testclasses_file": "reference/freight_calculator.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"nesting_depth",
"magic_number",
"dead_code"
]
}

View File

@@ -0,0 +1,14 @@
{
"id": "G0007",
"pool": "eval",
"object_type": "CLAS",
"category": "F",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,103 @@
CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_loyalty_rules.
METHODS setup.
METHODS tier_boundaries FOR TESTING.
METHODS tier_negative FOR TESTING.
METHODS tier_zero FOR TESTING.
METHODS discount_all_tiers FOR TESTING.
METHODS discount_unknown_tier FOR TESTING.
METHODS next_tier_from_basic FOR TESTING.
METHODS next_tier_from_silver FOR TESTING.
METHODS next_tier_from_gold FOR TESTING.
METHODS next_tier_from_platinum FOR TESTING.
METHODS next_tier_negative_points FOR TESTING.
ENDCLASS.
CLASS {{p}}t02_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}loyalty_evaluator( ).
ENDMETHOD.
METHOD tier_boundaries.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( 999 ) msg = `999` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-silver
act = mo_cut->get_tier( 1000 ) msg = `1000` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-silver
act = mo_cut->get_tier( 4999 ) msg = `4999` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-gold
act = mo_cut->get_tier( 5000 ) msg = `5000` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-gold
act = mo_cut->get_tier( 19999 ) msg = `19999` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-platinum
act = mo_cut->get_tier( 20000 ) msg = `20000` ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-platinum
act = mo_cut->get_tier( 500000 ) msg = `500000` ).
ENDMETHOD.
METHOD tier_negative.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( -1 ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( -5000 ) ).
ENDMETHOD.
METHOD tier_zero.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( 0 ) ).
ENDMETHOD.
METHOD discount_all_tiers.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-basic ) msg = `basic` ).
cl_abap_unit_assert=>assert_equals( exp = 3
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-silver ) msg = `silver` ).
cl_abap_unit_assert=>assert_equals( exp = 7
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-gold ) msg = `gold` ).
cl_abap_unit_assert=>assert_equals( exp = 12
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-platinum ) msg = `platinum` ).
ENDMETHOD.
METHOD discount_unknown_tier.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_discount( 'X' ) ).
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_discount( ' ' ) ).
ENDMETHOD.
METHOD next_tier_from_basic.
cl_abap_unit_assert=>assert_equals( exp = 1000
act = mo_cut->points_to_next_tier( 0 ) msg = `0` ).
cl_abap_unit_assert=>assert_equals( exp = 1
act = mo_cut->points_to_next_tier( 999 ) msg = `999` ).
ENDMETHOD.
METHOD next_tier_from_silver.
cl_abap_unit_assert=>assert_equals( exp = 4000
act = mo_cut->points_to_next_tier( 1000 ) msg = `1000` ).
cl_abap_unit_assert=>assert_equals( exp = 1
act = mo_cut->points_to_next_tier( 4999 ) msg = `4999` ).
ENDMETHOD.
METHOD next_tier_from_gold.
cl_abap_unit_assert=>assert_equals( exp = 15000
act = mo_cut->points_to_next_tier( 5000 ) msg = `5000` ).
cl_abap_unit_assert=>assert_equals( exp = 1
act = mo_cut->points_to_next_tier( 19999 ) msg = `19999` ).
ENDMETHOD.
METHOD next_tier_from_platinum.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->points_to_next_tier( 20000 ) ).
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->points_to_next_tier( 999999 ) ).
ENDMETHOD.
METHOD next_tier_negative_points.
cl_abap_unit_assert=>assert_equals( exp = 1000
act = mo_cut->points_to_next_tier( -7 ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,79 @@
CLASS {{p}}loyalty_evaluator DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_loyalty_rules.
PRIVATE SECTION.
CONSTANTS:
c_silver_threshold TYPE i VALUE 1000,
c_gold_threshold TYPE i VALUE 5000,
c_platinum_threshold TYPE i VALUE 20000.
METHODS normalize_points
IMPORTING iv_points TYPE i
RETURNING VALUE(rv_points) TYPE i.
METHODS tier_of
IMPORTING iv_points TYPE i
RETURNING VALUE(rv_tier) TYPE {{p}}if_loyalty_rules=>ty_tier.
METHODS points_to_tier
IMPORTING iv_points TYPE i
iv_tier TYPE {{p}}if_loyalty_rules=>ty_tier
RETURNING VALUE(rv_points) TYPE i.
ENDCLASS.
CLASS {{p}}loyalty_evaluator IMPLEMENTATION.
METHOD {{p}}if_loyalty_rules~get_tier.
rv_tier = tier_of( normalize_points( iv_points ) ).
ENDMETHOD.
METHOD {{p}}if_loyalty_rules~get_discount.
CASE iv_tier.
WHEN {{p}}if_loyalty_rules=>c_tier-silver.
rv_percent = 3.
WHEN {{p}}if_loyalty_rules=>c_tier-gold.
rv_percent = 7.
WHEN {{p}}if_loyalty_rules=>c_tier-platinum.
rv_percent = 12.
WHEN OTHERS.
rv_percent = 0.
ENDCASE.
ENDMETHOD.
METHOD {{p}}if_loyalty_rules~points_to_next_tier.
DATA(lv_points) = normalize_points( iv_points ).
DATA(lv_tier) = tier_of( lv_points ).
rv_points = points_to_tier( iv_points = lv_points
iv_tier = lv_tier ).
ENDMETHOD.
METHOD normalize_points.
rv_points = COND #( WHEN iv_points < 0 THEN 0 ELSE iv_points ).
ENDMETHOD.
METHOD tier_of.
IF iv_points >= c_platinum_threshold.
rv_tier = {{p}}if_loyalty_rules=>c_tier-platinum.
ELSEIF iv_points >= c_gold_threshold.
rv_tier = {{p}}if_loyalty_rules=>c_tier-gold.
ELSEIF iv_points >= c_silver_threshold.
rv_tier = {{p}}if_loyalty_rules=>c_tier-silver.
ELSE.
rv_tier = {{p}}if_loyalty_rules=>c_tier-basic.
ENDIF.
ENDMETHOD.
METHOD points_to_tier.
CASE iv_tier.
WHEN {{p}}if_loyalty_rules=>c_tier-basic.
rv_points = c_silver_threshold - iv_points.
WHEN {{p}}if_loyalty_rules=>c_tier-silver.
rv_points = c_gold_threshold - iv_points.
WHEN {{p}}if_loyalty_rules=>c_tier-gold.
rv_points = c_platinum_threshold - iv_points.
WHEN OTHERS.
rv_points = 0.
ENDCASE.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,77 @@
CLASS ltc_loyalty_evaluator DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_loyalty_rules.
METHODS setup.
METHODS tier_of_boundary_points FOR TESTING.
METHODS negative_points_are_basic FOR TESTING.
METHODS discount_of_each_tier FOR TESTING.
METHODS discount_of_unknown_tier FOR TESTING.
METHODS points_to_silver FOR TESTING.
METHODS points_to_gold FOR TESTING.
METHODS points_to_platinum FOR TESTING.
METHODS platinum_needs_no_points FOR TESTING.
ENDCLASS.
CLASS ltc_loyalty_evaluator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}loyalty_evaluator( ).
ENDMETHOD.
METHOD tier_of_boundary_points.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( 0 ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-silver
act = mo_cut->get_tier( 1000 ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-gold
act = mo_cut->get_tier( 5000 ) ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-platinum
act = mo_cut->get_tier( 20000 ) ).
ENDMETHOD.
METHOD negative_points_are_basic.
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_loyalty_rules=>c_tier-basic
act = mo_cut->get_tier( -100 ) ).
ENDMETHOD.
METHOD discount_of_each_tier.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-basic ) ).
cl_abap_unit_assert=>assert_equals( exp = 3
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-silver ) ).
cl_abap_unit_assert=>assert_equals( exp = 7
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-gold ) ).
cl_abap_unit_assert=>assert_equals( exp = 12
act = mo_cut->get_discount( {{p}}if_loyalty_rules=>c_tier-platinum ) ).
ENDMETHOD.
METHOD discount_of_unknown_tier.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->get_discount( 'X' ) ).
ENDMETHOD.
METHOD points_to_silver.
cl_abap_unit_assert=>assert_equals( exp = 1000
act = mo_cut->points_to_next_tier( 0 ) ).
cl_abap_unit_assert=>assert_equals( exp = 1
act = mo_cut->points_to_next_tier( 999 ) ).
ENDMETHOD.
METHOD points_to_gold.
cl_abap_unit_assert=>assert_equals( exp = 4000
act = mo_cut->points_to_next_tier( 1000 ) ).
ENDMETHOD.
METHOD points_to_platinum.
cl_abap_unit_assert=>assert_equals( exp = 15000
act = mo_cut->points_to_next_tier( 5000 ) ).
ENDMETHOD.
METHOD platinum_needs_no_points.
cl_abap_unit_assert=>assert_equals( exp = 0
act = mo_cut->points_to_next_tier( 20000 ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,24 @@
INTERFACE {{p}}if_loyalty_rules PUBLIC.
TYPES ty_points TYPE i.
TYPES ty_tier TYPE c LENGTH 1.
CONSTANTS:
BEGIN OF c_tier,
basic TYPE ty_tier VALUE 'B',
silver TYPE ty_tier VALUE 'S',
gold TYPE ty_tier VALUE 'G',
platinum TYPE ty_tier VALUE 'P',
END OF c_tier.
METHODS get_tier
IMPORTING iv_points TYPE ty_points
RETURNING VALUE(rv_tier) TYPE ty_tier.
METHODS get_discount
IMPORTING iv_tier TYPE ty_tier
RETURNING VALUE(rv_percent) TYPE i.
METHODS points_to_next_tier
IMPORTING iv_points TYPE ty_points
RETURNING VALUE(rv_points) TYPE i.
ENDINTERFACE.

View File

@@ -0,0 +1,57 @@
# 1. Goal
A retail company gives loyalty discounts to its customers. A checkout program needs
three values: the loyalty tier of a customer, the discount percent of a tier, and
the number of points that a customer still needs for the next tier. The program
calls one class for these three values.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_LOYALTY_RULES exists in package $TMP. It is active.
- The interface declares the type of a point value, the type of a tier, the
constants for all tiers, and three methods. This document does not repeat the
signature of the interface. Read the interface before you start.
- The three methods are:
* the method that returns the tier for a point value,
* the method that returns the discount percent of a tier,
* the method that returns the number of points that are missing for the next tier.
# 4. Contract
- Create the class {{P}}LOYALTY_EVALUATOR in package $TMP.
- The class is public and final. It has a public constructor without parameters.
- The class implements {{P}}IF_LOYALTY_RULES.
- Do not add other public methods.
# 5. Business rules
Tier of a point value:
1. A point value below zero counts as zero.
2. 0 to 999 points is the basic tier.
3. 1000 to 4999 points is the silver tier.
4. 5000 to 19999 points is the gold tier.
5. 20000 points and more is the platinum tier.
Discount percent of a tier:
6. basic is 0 percent.
7. silver is 3 percent.
8. gold is 7 percent.
9. platinum is 12 percent.
10. Any other tier value is 0 percent.
Points to the next tier:
11. A point value below zero counts as zero.
12. basic needs 1000 points minus the point value.
13. silver needs 5000 points minus the point value.
14. gold needs 20000 points minus the point value.
15. platinum needs 0 points.
# 6. Constraints
- Release target: 8.16.
- Coding standards: Clean ABAP. Methods below 40 statements. No global variables.
No comment that restates the code.
- Out of scope: do not change {{P}}IF_LOYALTY_RULES.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,51 @@
{
"id": "G0007",
"category": "F",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_LOYALTY_RULES",
"file": "seed/if_loyalty_rules.intf.abap",
"description": "Loyalty contract: tier type, point type, tier constants and three methods. The spec does not repeat the signature."
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}LOYALTY_EVALUATOR",
"implements": "{{P}}IF_LOYALTY_RULES"
}
],
"out_of_scope": [
"{{P}}IF_LOYALTY_RULES"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}LOYALTY_EVALUATOR",
"file": "reference/loyalty_evaluator.clas.abap",
"description": "Loyalty evaluator",
"testclasses_file": "reference/loyalty_evaluator.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"nesting_depth"
]
}

View File

@@ -0,0 +1,19 @@
{
"id": "G0008",
"pool": "eval",
"object_type": "CLAS",
"category": "G",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,197 @@
CLASS {{p}}t07_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_freight_calc.
METHODS setup.
METHODS assert_amount
IMPORTING iv_expected TYPE {{p}}if_freight_calc=>ty_amount
iv_actual TYPE {{p}}if_freight_calc=>ty_amount.
METHODS assert_weight
IMPORTING iv_expected TYPE {{p}}if_freight_calc=>ty_weight
iv_actual TYPE {{p}}if_freight_calc=>ty_weight.
METHODS weight_not_positive FOR TESTING.
METHODS volume_negative FOR TESTING.
METHODS distance_not_positive FOR TESTING.
METHODS fuel_index_negative FOR TESTING.
METHODS fuel_index_too_high FOR TESTING.
METHODS base_at_weight_limit FOR TESTING.
METHODS base_above_weight_limit FOR TESTING.
METHODS volumetric_weight_wins FOR TESTING.
METHODS distance_component FOR TESTING.
METHODS fuel_rounds_half_up FOR TESTING.
METHODS full_load FOR TESTING.
ENDCLASS.
CLASS {{p}}t07_hidden IMPLEMENTATION.
METHOD setup.
DATA lo_calc TYPE REF TO {{p}}freight_calc.
CREATE OBJECT lo_calc.
mo_cut = lo_calc.
ENDMETHOD.
METHOD assert_amount.
cl_abap_unit_assert=>assert_equals( exp = iv_expected act = iv_actual ).
ENDMETHOD.
METHOD assert_weight.
cl_abap_unit_assert=>assert_equals( exp = iv_expected act = iv_actual ).
ENDMETHOD.
METHOD weight_not_positive.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 0
iv_volume_m3 = -1
iv_distance_km = 0
iv_fuel_index = 200 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-weight
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
assert_weight( iv_expected = 0 iv_actual = ls_result-chargeable_weight ).
ENDMETHOD.
METHOD volume_negative.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = '-0.5'
iv_distance_km = 100
iv_fuel_index = 50 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-volume
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD distance_not_positive.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 0
iv_fuel_index = 50 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-distance
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD fuel_index_negative.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = '-0.1' ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-fuel
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD fuel_index_too_high.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = '100.1' ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-fuel
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD base_at_weight_limit.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = 0 ).
assert_weight( iv_expected = 1000 iv_actual = ls_result-chargeable_weight ).
assert_amount( iv_expected = 45 iv_actual = ls_result-base ).
assert_amount( iv_expected = 0 iv_actual = ls_result-distance ).
assert_amount( iv_expected = 0 iv_actual = ls_result-fuel ).
assert_amount( iv_expected = 45 iv_actual = ls_result-total ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-ok
act = ls_result-reason ).
ENDMETHOD.
METHOD base_above_weight_limit.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = '1100.5'
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = 0 ).
assert_weight( iv_expected = '1100.5' iv_actual = ls_result-chargeable_weight ).
assert_amount( iv_expected = 58 iv_actual = ls_result-base ).
assert_amount( iv_expected = 58 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD volumetric_weight_wins.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 500
iv_volume_m3 = '4.5'
iv_distance_km = 100
iv_fuel_index = 0 ).
assert_weight( iv_expected = 1125 iv_actual = ls_result-chargeable_weight ).
assert_amount( iv_expected = 58 iv_actual = ls_result-base ).
assert_amount( iv_expected = 58 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD distance_component.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 350
iv_fuel_index = 0 ).
assert_amount( iv_expected = 45 iv_actual = ls_result-base ).
assert_amount( iv_expected = 27 iv_actual = ls_result-distance ).
assert_amount( iv_expected = 72 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD fuel_rounds_half_up.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = '12.3' ).
assert_amount( iv_expected = 45 iv_actual = ls_result-base ).
assert_amount( iv_expected = 0 iv_actual = ls_result-distance ).
assert_amount( iv_expected = '5.54' iv_actual = ls_result-fuel ).
assert_amount( iv_expected = '50.54' iv_actual = ls_result-total ).
ENDMETHOD.
METHOD full_load.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 2000
iv_volume_m3 = 1
iv_distance_km = 500
iv_fuel_index = 100 ).
assert_weight( iv_expected = 2000 iv_actual = ls_result-chargeable_weight ).
assert_amount( iv_expected = 110 iv_actual = ls_result-base ).
assert_amount( iv_expected = 36 iv_actual = ls_result-distance ).
assert_amount( iv_expected = 146 iv_actual = ls_result-fuel ).
assert_amount( iv_expected = 292 iv_actual = ls_result-total ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-ok
act = ls_result-reason ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,149 @@
CLASS {{p}}freight_calc DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_freight_calc.
PRIVATE SECTION.
TYPES ty_quantity TYPE p LENGTH 13 DECIMALS 3.
CONSTANTS:
c_base_amount TYPE {{p}}if_freight_calc=>ty_amount VALUE '45.00',
c_base_weight TYPE ty_quantity VALUE '1000.000',
c_weight_rate TYPE {{p}}if_freight_calc=>ty_amount VALUE '6.50',
c_free_km TYPE i VALUE 100,
c_km_rate TYPE {{p}}if_freight_calc=>ty_amount VALUE '9.00',
c_step TYPE ty_quantity VALUE '100.000',
c_volume_factor TYPE i VALUE 250,
c_percent TYPE p LENGTH 3 DECIMALS 2 VALUE '0.01'.
METHODS check_input
IMPORTING iv_weight_kg TYPE {{p}}if_freight_calc=>ty_weight
iv_volume_m3 TYPE {{p}}if_freight_calc=>ty_volume
iv_distance_km TYPE {{p}}if_freight_calc=>ty_distance
iv_fuel_index TYPE {{p}}if_freight_calc=>ty_fuel
RETURNING VALUE(rv_reason) TYPE {{p}}if_freight_calc=>ty_reason.
METHODS chargeable_weight
IMPORTING iv_weight_kg TYPE {{p}}if_freight_calc=>ty_weight
iv_volume_m3 TYPE {{p}}if_freight_calc=>ty_volume
RETURNING VALUE(rv_weight) TYPE ty_quantity.
METHODS base_freight
IMPORTING iv_weight_kg TYPE ty_quantity
RETURNING VALUE(rv_amount) TYPE {{p}}if_freight_calc=>ty_amount.
METHODS distance_freight
IMPORTING iv_distance_km TYPE {{p}}if_freight_calc=>ty_distance
RETURNING VALUE(rv_amount) TYPE {{p}}if_freight_calc=>ty_amount.
METHODS fuel_freight
IMPORTING iv_amount TYPE {{p}}if_freight_calc=>ty_amount
iv_fuel_index TYPE {{p}}if_freight_calc=>ty_fuel
RETURNING VALUE(rv_amount) TYPE {{p}}if_freight_calc=>ty_amount.
METHODS started_steps
IMPORTING iv_excess TYPE ty_quantity
RETURNING VALUE(rv_steps) TYPE i.
ENDCLASS.
CLASS {{p}}freight_calc IMPLEMENTATION.
METHOD {{p}}if_freight_calc~calculate.
DATA lv_reason TYPE {{p}}if_freight_calc=>ty_reason.
DATA lv_weight TYPE ty_quantity.
DATA lv_net TYPE {{p}}if_freight_calc=>ty_amount.
CLEAR rs_result.
lv_reason = check_input( iv_weight_kg = iv_weight_kg
iv_volume_m3 = iv_volume_m3
iv_distance_km = iv_distance_km
iv_fuel_index = iv_fuel_index ).
IF lv_reason <> {{p}}if_freight_calc=>c_reason-ok.
rs_result-reason = lv_reason.
RETURN.
ENDIF.
lv_weight = chargeable_weight( iv_weight_kg = iv_weight_kg
iv_volume_m3 = iv_volume_m3 ).
rs_result-chargeable_weight = lv_weight.
rs_result-base = base_freight( lv_weight ).
rs_result-distance = distance_freight( iv_distance_km ).
lv_net = rs_result-base + rs_result-distance.
rs_result-fuel = fuel_freight( iv_amount = lv_net
iv_fuel_index = iv_fuel_index ).
rs_result-total = rs_result-base + rs_result-distance + rs_result-fuel.
rs_result-reason = {{p}}if_freight_calc=>c_reason-ok.
ENDMETHOD.
METHOD check_input.
IF iv_weight_kg <= 0.
rv_reason = {{p}}if_freight_calc=>c_reason-weight.
ELSEIF iv_volume_m3 < 0.
rv_reason = {{p}}if_freight_calc=>c_reason-volume.
ELSEIF iv_distance_km <= 0.
rv_reason = {{p}}if_freight_calc=>c_reason-distance.
ELSEIF iv_fuel_index < 0 OR iv_fuel_index > 100.
rv_reason = {{p}}if_freight_calc=>c_reason-fuel.
ELSE.
rv_reason = {{p}}if_freight_calc=>c_reason-ok.
ENDIF.
ENDMETHOD.
METHOD chargeable_weight.
DATA lv_volumetric TYPE ty_quantity.
lv_volumetric = iv_volume_m3 * c_volume_factor.
IF lv_volumetric > iv_weight_kg.
rv_weight = lv_volumetric.
ELSE.
rv_weight = iv_weight_kg.
ENDIF.
ENDMETHOD.
METHOD base_freight.
DATA lv_excess TYPE ty_quantity.
DATA lv_steps TYPE i.
lv_excess = iv_weight_kg - c_base_weight.
IF lv_excess <= 0.
rv_amount = c_base_amount.
ELSE.
lv_steps = started_steps( lv_excess ).
rv_amount = c_base_amount + lv_steps * c_weight_rate.
ENDIF.
ENDMETHOD.
METHOD distance_freight.
DATA lv_excess TYPE ty_quantity.
DATA lv_steps TYPE i.
lv_excess = iv_distance_km - c_free_km.
IF lv_excess <= 0.
rv_amount = 0.
ELSE.
lv_steps = started_steps( lv_excess ).
rv_amount = lv_steps * c_km_rate.
ENDIF.
ENDMETHOD.
METHOD fuel_freight.
DATA lv_scaled TYPE p LENGTH 15 DECIMALS 3.
DATA lv_fuel TYPE p LENGTH 15 DECIMALS 5.
lv_scaled = iv_amount * iv_fuel_index.
lv_fuel = lv_scaled * c_percent.
rv_amount = lv_fuel.
ENDMETHOD.
METHOD started_steps.
DATA lv_excess_scaled TYPE p LENGTH 16 DECIMALS 0.
DATA lv_step_scaled TYPE p LENGTH 16 DECIMALS 0.
lv_excess_scaled = iv_excess * 1000.
lv_step_scaled = c_step * 1000.
rv_steps = lv_excess_scaled DIV lv_step_scaled.
IF lv_excess_scaled MOD lv_step_scaled > 0.
rv_steps = rv_steps + 1.
ENDIF.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,114 @@
CLASS ltc_freight_calc DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_freight_calc.
METHODS setup.
METHODS assert_amount
IMPORTING iv_expected TYPE {{p}}if_freight_calc=>ty_amount
iv_actual TYPE {{p}}if_freight_calc=>ty_amount.
METHODS assert_weight
IMPORTING iv_expected TYPE {{p}}if_freight_calc=>ty_weight
iv_actual TYPE {{p}}if_freight_calc=>ty_weight.
METHODS base_and_distance FOR TESTING.
METHODS fuel_is_rounded FOR TESTING.
METHODS volume_sets_chargeable_weight FOR TESTING.
METHODS weight_zero_is_rejected FOR TESTING.
METHODS distance_zero_is_rejected FOR TESTING.
METHODS fuel_index_out_of_range FOR TESTING.
ENDCLASS.
CLASS ltc_freight_calc IMPLEMENTATION.
METHOD setup.
DATA lo_calc TYPE REF TO {{p}}freight_calc.
CREATE OBJECT lo_calc.
mo_cut = lo_calc.
ENDMETHOD.
METHOD assert_amount.
cl_abap_unit_assert=>assert_equals( exp = iv_expected act = iv_actual ).
ENDMETHOD.
METHOD assert_weight.
cl_abap_unit_assert=>assert_equals( exp = iv_expected act = iv_actual ).
ENDMETHOD.
METHOD base_and_distance.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 2000
iv_volume_m3 = 1
iv_distance_km = 500
iv_fuel_index = 100 ).
assert_amount( iv_expected = 110 iv_actual = ls_result-base ).
assert_amount( iv_expected = 36 iv_actual = ls_result-distance ).
assert_amount( iv_expected = 146 iv_actual = ls_result-fuel ).
assert_amount( iv_expected = 292 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD fuel_is_rounded.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1000
iv_volume_m3 = 0
iv_distance_km = 100
iv_fuel_index = '12.3' ).
assert_amount( iv_expected = '5.54' iv_actual = ls_result-fuel ).
assert_amount( iv_expected = '50.54' iv_actual = ls_result-total ).
ENDMETHOD.
METHOD volume_sets_chargeable_weight.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 500
iv_volume_m3 = '4.5'
iv_distance_km = 100
iv_fuel_index = 0 ).
assert_weight( iv_expected = 1125 iv_actual = ls_result-chargeable_weight ).
assert_amount( iv_expected = 58 iv_actual = ls_result-base ).
ENDMETHOD.
METHOD weight_zero_is_rejected.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 0
iv_volume_m3 = 2
iv_distance_km = 300
iv_fuel_index = 10 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-weight
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD distance_zero_is_rejected.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1500
iv_volume_m3 = 0
iv_distance_km = 0
iv_fuel_index = 10 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-distance
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
METHOD fuel_index_out_of_range.
DATA ls_result TYPE {{p}}if_freight_calc=>ty_result.
ls_result = mo_cut->calculate( iv_weight_kg = 1500
iv_volume_m3 = 0
iv_distance_km = 300
iv_fuel_index = 120 ).
cl_abap_unit_assert=>assert_equals( exp = {{p}}if_freight_calc=>c_reason-fuel
act = ls_result-reason ).
assert_amount( iv_expected = 0 iv_actual = ls_result-total ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,34 @@
INTERFACE {{p}}if_freight_calc PUBLIC.
TYPES ty_reason TYPE c LENGTH 10.
TYPES ty_amount TYPE p LENGTH 9 DECIMALS 2.
TYPES ty_weight TYPE p LENGTH 9 DECIMALS 3.
TYPES ty_volume TYPE p LENGTH 9 DECIMALS 3.
TYPES ty_distance TYPE i.
TYPES ty_fuel TYPE p LENGTH 5 DECIMALS 1.
CONSTANTS:
BEGIN OF c_reason,
ok TYPE ty_reason VALUE 'OK',
weight TYPE ty_reason VALUE 'WEIGHT',
volume TYPE ty_reason VALUE 'VOLUME',
distance TYPE ty_reason VALUE 'DISTANCE',
fuel TYPE ty_reason VALUE 'FUEL',
END OF c_reason.
TYPES:
BEGIN OF ty_result,
chargeable_weight TYPE ty_weight,
base TYPE ty_amount,
distance TYPE ty_amount,
fuel TYPE ty_amount,
total TYPE ty_amount,
reason TYPE ty_reason,
END OF ty_result.
METHODS calculate
IMPORTING iv_weight_kg TYPE ty_weight
iv_volume_m3 TYPE ty_volume
iv_distance_km TYPE ty_distance
iv_fuel_index TYPE ty_fuel
RETURNING VALUE(rs_result) TYPE ty_result.
ENDINTERFACE.

View File

@@ -0,0 +1,61 @@
# 1. Goal
A freight forwarder calculates the price of an outbound shipment. The
calculation service receives the gross weight, the volume, the transport
distance, and the current fuel index. It returns the freight components and a
reason code.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_FREIGHT_CALC exists in package $TMP. It is active.
- The interface defines the types TY_REASON, TY_AMOUNT, TY_WEIGHT, TY_VOLUME,
TY_DISTANCE, TY_FUEL, and TY_RESULT.
- The interface defines the constant structure C_REASON with the components
OK, WEIGHT, VOLUME, DISTANCE, and FUEL.
- The interface defines the instance method CALCULATE with the importing
parameters IV_WEIGHT_KG, IV_VOLUME_M3, IV_DISTANCE_KM, IV_FUEL_INDEX and the
returning parameter RS_RESULT.
# 4. Contract
- Create the class {{P}}FREIGHT_CALC in package $TMP.
- The class is public, final, and has a public constructor without parameters.
- The class implements {{P}}IF_FREIGHT_CALC.
- Do not add other public methods.
# 5. Business rules
All amounts are in euro. Apply the rules in this sequence.
1. If IV_WEIGHT_KG is less than or equal to 0, return the reason WEIGHT. Set
all amounts and the chargeable weight to 0.
2. If IV_VOLUME_M3 is less than 0, return the reason VOLUME. Set all amounts
and the chargeable weight to 0.
3. If IV_DISTANCE_KM is less than or equal to 0, return the reason DISTANCE.
Set all amounts and the chargeable weight to 0.
4. If IV_FUEL_INDEX is less than 0 or greater than 100, return the reason FUEL.
Set all amounts and the chargeable weight to 0.
5. The volumetric weight is IV_VOLUME_M3 multiplied by 250.
6. The chargeable weight is the greater of IV_WEIGHT_KG and the volumetric
weight.
7. The base freight is 45.00. For each full or partial 100 kg by which the
chargeable weight is greater than 1000 kg, add 6.50.
8. The distance freight is 0.00 for a distance up to and including 100 km. For
each full or partial 100 km by which IV_DISTANCE_KM is greater than 100 km,
add 9.00.
9. The fuel freight is the sum of the base freight and the distance freight,
multiplied by IV_FUEL_INDEX, divided by 100. Round the result to 2 decimal
places. Round a half away from zero.
10. The total is the sum of the base freight, the distance freight, and the
fuel freight.
11. Return the reason OK.
# 6. Constraints
- Release target: 7.40 SP05. Do not use syntax that is newer than 7.40 SP05.
For example, do not use COND, SWITCH, REDUCE, FOR, or LET.
- Coding standards: Clean ABAP. Method length below 40 statements. No global
variables. No comment that restates the code.
- Out of scope: do not change {{P}}IF_FREIGHT_CALC.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,51 @@
{
"id": "G0008",
"category": "G",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v740sp05",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 45,
"max_activations": 12
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_FREIGHT_CALC",
"file": "seed/if_freight_calc.intf.abap",
"description": "Freight calculation contract with result type and reason codes"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALC",
"implements": "{{P}}IF_FREIGHT_CALC"
}
],
"out_of_scope": [
"{{P}}IF_FREIGHT_CALC"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T07_HIDDEN",
"file": "hidden/t07_hidden.clas.abap",
"description": "T07 hidden tests for the freight calculation"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}FREIGHT_CALC",
"file": "reference/freight_calc.clas.abap",
"description": "Freight surcharge calculator",
"testclasses_file": "reference/freight_calc.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"nesting_depth"
]
}

View File

@@ -0,0 +1,19 @@
{
"id": "G0009",
"pool": "eval",
"object_type": "CLAS",
"category": "I",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,111 @@
CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_shipping_cost.
METHODS setup.
METHODS zero_weight_is_free FOR TESTING.
METHODS light_parcel_gets_minimum FOR TESTING.
METHODS tier_limit_one_kg FOR TESTING.
METHODS tier_limit_five_kg FOR TESTING.
METHODS tier_limit_ten_kg FOR TESTING.
METHODS bulk_weight_limit FOR TESTING.
METHODS bulk_weight_started_kg FOR TESTING.
METHODS express_surcharge FOR TESTING.
METHODS express_below_minimum FOR TESTING.
METHODS bulk_express FOR TESTING.
METHODS assert_cost
IMPORTING iv_weight TYPE {{p}}if_shipping_cost=>ty_weight
iv_express TYPE abap_bool
iv_exp TYPE {{p}}if_shipping_cost=>ty_amount
iv_msg TYPE string.
ENDCLASS.
CLASS {{p}}t02_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}shipping_cost_calc( ).
ENDMETHOD.
METHOD assert_cost.
cl_abap_unit_assert=>assert_equals(
exp = iv_exp
act = mo_cut->calculate( iv_weight = iv_weight iv_express = iv_express )
msg = iv_msg ).
ENDMETHOD.
METHOD zero_weight_is_free.
assert_cost( iv_weight = '0.000' iv_express = abap_false
iv_exp = '0.00' iv_msg = `zero weight` ).
assert_cost( iv_weight = '-2.500' iv_express = abap_true
iv_exp = '0.00' iv_msg = `negative weight` ).
ENDMETHOD.
METHOD light_parcel_gets_minimum.
assert_cost( iv_weight = '0.100' iv_express = abap_false
iv_exp = '6.00' iv_msg = `0.100 kg` ).
assert_cost( iv_weight = '0.500' iv_express = abap_false
iv_exp = '6.00' iv_msg = `0.500 kg` ).
ENDMETHOD.
METHOD tier_limit_one_kg.
assert_cost( iv_weight = '1.000' iv_express = abap_false
iv_exp = '6.00' iv_msg = `1.000 kg` ).
assert_cost( iv_weight = '1.001' iv_express = abap_false
iv_exp = '8.50' iv_msg = `1.001 kg` ).
ENDMETHOD.
METHOD tier_limit_five_kg.
assert_cost( iv_weight = '5.000' iv_express = abap_false
iv_exp = '8.50' iv_msg = `5.000 kg` ).
assert_cost( iv_weight = '5.001' iv_express = abap_false
iv_exp = '12.00' iv_msg = `5.001 kg` ).
ENDMETHOD.
METHOD tier_limit_ten_kg.
assert_cost( iv_weight = '10.000' iv_express = abap_false
iv_exp = '12.00' iv_msg = `10.000 kg` ).
assert_cost( iv_weight = '10.001' iv_express = abap_false
iv_exp = '18.75' iv_msg = `10.001 kg` ).
ENDMETHOD.
METHOD bulk_weight_limit.
assert_cost( iv_weight = '31.500' iv_express = abap_false
iv_exp = '18.75' iv_msg = `31.500 kg` ).
assert_cost( iv_weight = '31.501' iv_express = abap_false
iv_exp = '19.40' iv_msg = `31.501 kg` ).
ENDMETHOD.
METHOD bulk_weight_started_kg.
assert_cost( iv_weight = '32.000' iv_express = abap_false
iv_exp = '19.40' iv_msg = `32.000 kg` ).
assert_cost( iv_weight = '33.000' iv_express = abap_false
iv_exp = '20.05' iv_msg = `33.000 kg` ).
assert_cost( iv_weight = '100.000' iv_express = abap_false
iv_exp = '63.60' iv_msg = `100.000 kg` ).
ENDMETHOD.
METHOD express_surcharge.
assert_cost( iv_weight = '2.000' iv_express = abap_true
iv_exp = '11.48' iv_msg = `2.000 kg express` ).
assert_cost( iv_weight = '20.000' iv_express = abap_true
iv_exp = '25.31' iv_msg = `20.000 kg express` ).
ENDMETHOD.
METHOD express_below_minimum.
assert_cost( iv_weight = '0.500' iv_express = abap_true
iv_exp = '6.62' iv_msg = `0.500 kg express` ).
assert_cost( iv_weight = '1.000' iv_express = abap_true
iv_exp = '6.62' iv_msg = `1.000 kg express` ).
ENDMETHOD.
METHOD bulk_express.
assert_cost( iv_weight = '32.000' iv_express = abap_true
iv_exp = '26.19' iv_msg = `32.000 kg express` ).
assert_cost( iv_weight = '100.000' iv_express = abap_true
iv_exp = '85.86' iv_msg = `100.000 kg express` ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,56 @@
CLASS {{p}}shipping_cost_calc DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_shipping_cost.
PRIVATE SECTION.
METHODS base_rate
IMPORTING iv_weight TYPE {{p}}if_shipping_cost=>ty_weight
RETURNING VALUE(rv_rate) TYPE {{p}}if_shipping_cost=>ty_amount.
METHODS express_surcharge
IMPORTING iv_rate TYPE {{p}}if_shipping_cost=>ty_amount
RETURNING VALUE(rv_surcharge) TYPE {{p}}if_shipping_cost=>ty_amount.
ENDCLASS.
CLASS {{p}}shipping_cost_calc IMPLEMENTATION.
METHOD {{p}}if_shipping_cost~calculate.
IF iv_weight <= 0.
rv_cost = 0.
RETURN.
ENDIF.
DATA(lv_cost) = base_rate( iv_weight ).
IF iv_express = abap_true.
lv_cost = lv_cost + express_surcharge( lv_cost ).
ENDIF.
IF lv_cost < {{p}}if_shipping_cost=>c_rate-min_charge.
rv_cost = {{p}}if_shipping_cost=>c_rate-min_charge.
ELSE.
rv_cost = lv_cost.
ENDIF.
ENDMETHOD.
METHOD base_rate.
IF iv_weight <= 1.
rv_rate = '4.90'.
ELSEIF iv_weight <= 5.
rv_rate = '8.50'.
ELSEIF iv_weight <= 10.
rv_rate = '12.00'.
ELSEIF iv_weight <= {{p}}if_shipping_cost=>c_rate-bulk_weight.
rv_rate = '18.75'.
ELSE.
DATA(lv_started_kg) = CONV i(
ceil( iv_weight - {{p}}if_shipping_cost=>c_rate-bulk_weight ) ).
rv_rate = '18.75' + lv_started_kg * {{p}}if_shipping_cost=>c_rate-bulk_step.
ENDIF.
ENDMETHOD.
METHOD express_surcharge.
rv_surcharge = round( val = iv_rate * {{p}}if_shipping_cost=>c_rate-express_percent / 100
dec = 2 ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,68 @@
CLASS ltc_shipping_cost_calc DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_shipping_cost.
METHODS setup.
METHODS zero_weight_is_free FOR TESTING.
METHODS minimum_charge FOR TESTING.
METHODS tier_limits FOR TESTING.
METHODS bulk_weight FOR TESTING.
METHODS express_surcharge FOR TESTING.
METHODS express_below_minimum FOR TESTING.
METHODS assert_cost
IMPORTING iv_weight TYPE {{p}}if_shipping_cost=>ty_weight
iv_express TYPE abap_bool
iv_exp TYPE {{p}}if_shipping_cost=>ty_amount
iv_msg TYPE string.
ENDCLASS.
CLASS ltc_shipping_cost_calc IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}shipping_cost_calc( ).
ENDMETHOD.
METHOD assert_cost.
cl_abap_unit_assert=>assert_equals(
exp = iv_exp
act = mo_cut->calculate( iv_weight = iv_weight iv_express = iv_express )
msg = iv_msg ).
ENDMETHOD.
METHOD zero_weight_is_free.
assert_cost( iv_weight = '0.000' iv_express = abap_true
iv_exp = '0.00' iv_msg = `zero weight` ).
ENDMETHOD.
METHOD minimum_charge.
assert_cost( iv_weight = '0.500' iv_express = abap_false
iv_exp = '6.00' iv_msg = `minimum charge` ).
ENDMETHOD.
METHOD tier_limits.
assert_cost( iv_weight = '1.000' iv_express = abap_false
iv_exp = '6.00' iv_msg = `1.000 kg` ).
assert_cost( iv_weight = '1.001' iv_express = abap_false
iv_exp = '8.50' iv_msg = `1.001 kg` ).
ENDMETHOD.
METHOD bulk_weight.
assert_cost( iv_weight = '31.500' iv_express = abap_false
iv_exp = '18.75' iv_msg = `31.500 kg` ).
assert_cost( iv_weight = '32.000' iv_express = abap_false
iv_exp = '19.40' iv_msg = `32.000 kg` ).
ENDMETHOD.
METHOD express_surcharge.
assert_cost( iv_weight = '2.000' iv_express = abap_true
iv_exp = '11.48' iv_msg = `2.000 kg express` ).
ENDMETHOD.
METHOD express_below_minimum.
assert_cost( iv_weight = '0.500' iv_express = abap_true
iv_exp = '6.62' iv_msg = `0.500 kg express` ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,17 @@
INTERFACE {{p}}if_shipping_cost PUBLIC.
TYPES ty_weight TYPE p LENGTH 13 DECIMALS 3.
TYPES ty_amount TYPE p LENGTH 15 DECIMALS 2.
CONSTANTS:
BEGIN OF c_rate,
min_charge TYPE ty_amount VALUE '6.00',
express_percent TYPE i VALUE 35,
bulk_weight TYPE ty_weight VALUE '31.500',
bulk_step TYPE ty_amount VALUE '0.65',
END OF c_rate.
METHODS calculate
IMPORTING iv_weight TYPE ty_weight
iv_express TYPE abap_bool
RETURNING VALUE(rv_cost) TYPE ty_amount.
ENDINTERFACE.

View File

@@ -0,0 +1,53 @@
CLASS {{p}}shipping_cost_calc DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_shipping_cost.
PRIVATE SECTION.
METHODS base_rate
IMPORTING iv_weight TYPE {{p}}if_shipping_cost=>ty_weight
RETURNING VALUE(rv_rate) TYPE {{p}}if_shipping_cost=>ty_amount.
METHODS express_surcharge
IMPORTING iv_rate TYPE {{p}}if_shipping_cost=>ty_amount
RETURNING VALUE(rv_surcharge) TYPE {{p}}if_shipping_cost=>ty_amount.
ENDCLASS.
CLASS {{p}}shipping_cost_calc IMPLEMENTATION.
METHOD {{p}}if_shipping_cost~calculate.
IF iv_weight <= 0.
RETURN.
ENDIF.
rv_cost = base_rate( iv_weight ).
IF rv_cost < {{p}}if_shipping_cost=>c_rate-min_charge.
rv_cost = {{p}}if_shipping_cost=>c_rate-min_charge.
ENDIF.
IF iv_express = abap_true.
rv_cost = rv_cost + express_surcharge( rv_cost ).
ENDIF.
ENDMETHOD.
METHOD base_rate.
IF iv_weight < 1.
rv_rate = '4.90'.
ELSEIF iv_weight < 5.
rv_rate = '8.50'.
ELSEIF iv_weight < 10.
rv_rate = '12.00'.
ELSEIF iv_weight < {{p}}if_shipping_cost=>c_rate-bulk_weight.
rv_rate = '18.75'.
ELSE.
DATA lv_started_kg TYPE i.
lv_started_kg = iv_weight - {{p}}if_shipping_cost=>c_rate-bulk_weight.
rv_rate = '18.75' + lv_started_kg * {{p}}if_shipping_cost=>c_rate-bulk_step.
ENDIF.
ENDMETHOD.
METHOD express_surcharge.
rv_surcharge = round( val = iv_rate * {{p}}if_shipping_cost=>c_rate-express_percent / 100
dec = 2 ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,53 @@
CLASS {{p}}shipping_cost_legacy DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_shipping_cost.
PRIVATE SECTION.
METHODS base_rate
IMPORTING iv_weight TYPE {{p}}if_shipping_cost=>ty_weight
RETURNING VALUE(rv_rate) TYPE {{p}}if_shipping_cost=>ty_amount.
METHODS express_surcharge
IMPORTING iv_rate TYPE {{p}}if_shipping_cost=>ty_amount
RETURNING VALUE(rv_surcharge) TYPE {{p}}if_shipping_cost=>ty_amount.
ENDCLASS.
CLASS {{p}}shipping_cost_legacy IMPLEMENTATION.
METHOD {{p}}if_shipping_cost~calculate.
IF iv_weight <= 0.
RETURN.
ENDIF.
rv_cost = base_rate( iv_weight ).
IF rv_cost < {{p}}if_shipping_cost=>c_rate-min_charge.
rv_cost = {{p}}if_shipping_cost=>c_rate-min_charge.
ENDIF.
IF iv_express = abap_true.
rv_cost = rv_cost + express_surcharge( rv_cost ).
ENDIF.
ENDMETHOD.
METHOD base_rate.
IF iv_weight < 1.
rv_rate = '4.90'.
ELSEIF iv_weight < 5.
rv_rate = '8.50'.
ELSEIF iv_weight < 10.
rv_rate = '12.00'.
ELSEIF iv_weight < {{p}}if_shipping_cost=>c_rate-bulk_weight.
rv_rate = '18.75'.
ELSE.
DATA lv_started_kg TYPE i.
lv_started_kg = iv_weight - {{p}}if_shipping_cost=>c_rate-bulk_weight.
rv_rate = '18.75' + lv_started_kg * {{p}}if_shipping_cost=>c_rate-bulk_step.
ENDIF.
ENDMETHOD.
METHOD express_surcharge.
rv_surcharge = round( val = iv_rate * {{p}}if_shipping_cost=>c_rate-express_percent / 100
dec = 2 ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,63 @@
# 1. Goal
Calculate the shipping cost of a parcel. A webshop calls the calculation before
it shows the total price to the customer. The cost depends on the weight of the
parcel and on an express flag. The class that the webshop uses today returns
wrong costs. A corrected class must replace it.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_SHIPPING_COST exists in package $TMP. It is active.
- The interface contains the types TY_WEIGHT (packed, 3 decimals) and TY_AMOUNT
(packed, 2 decimals), the constant structure C_RATE, and one method:
CALCULATE IMPORTING iv_weight TYPE ty_weight, iv_express TYPE abap_bool
RETURNING VALUE(rv_cost) TYPE ty_amount.
- C_RATE has the components MIN_CHARGE (6.00), EXPRESS_PERCENT (35),
BULK_WEIGHT (31.500), and BULK_STEP (0.65).
- The class {{P}}SHIPPING_COST_LEGACY exists in package $TMP. It is active and
it implements the interface, but the calculated costs are wrong.
- Reported symptoms of {{P}}SHIPPING_COST_LEGACY:
1. A parcel with a weight that is exactly on a weight limit is charged with
the rate of the next weight limit.
2. A parcel that is heavier than C_RATE-BULK_WEIGHT is charged as if its
weight were exactly C_RATE-BULK_WEIGHT.
3. An express parcel that is lighter than the minimum charge gets a total
that is too high.
# 4. Contract
- Create the class {{P}}SHIPPING_COST_CALC in package $TMP.
- The class is public and final and has a public constructor without parameters.
- The class implements {{P}}IF_SHIPPING_COST.
- Do not add other public methods.
- Do not change {{P}}IF_SHIPPING_COST and {{P}}SHIPPING_COST_LEGACY.
# 5. Business rules
Apply the rules in this sequence.
1. If iv_weight is zero or negative, the cost is 0. Stop.
2. Else calculate the base rate from the weight:
- weight up to and including 1.000 kg: 4.90
- weight up to and including 5.000 kg: 8.50
- weight up to and including 10.000 kg: 12.00
- weight up to and including 31.500 kg: 18.75
- weight above 31.500 kg: 18.75 plus 0.65 for each started kilogram above
31.500 kg. A started kilogram is a kilogram that is not complete.
Example: 31.501 kg is 0.001 kg above 31.500 kg, so there is one started
kilogram. 33.000 kg is 1.500 kg above 31.500 kg, so there are two started
kilograms.
3. If iv_express is abap_true, add 35 percent of the base rate to the base rate.
Round this surcharge to 2 decimals. Round a half away from zero.
4. If the result is below 6.00, the result is 6.00.
5. The result has 2 decimals.
# 6. Constraints
- Release target: 8.16.
- Coding standards: Clean ABAP. Methods below 40 statements. No global variables.
No comment that restates the code.
- Out of scope: do not change {{P}}IF_SHIPPING_COST and
{{P}}SHIPPING_COST_LEGACY.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,59 @@
{
"id": "G0009",
"category": "I",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 50,
"max_activations": 12
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_SHIPPING_COST",
"file": "seed/if_shipping_cost.intf.abap",
"description": "Shipping cost contract with the weight and amount types, the rate constants and the CALCULATE method"
},
{
"type": "CLAS",
"name": "{{P}}SHIPPING_COST_LEGACY",
"file": "seed/shipping_cost_legacy.clas.abap",
"description": "Active class that implements the interface but calculates wrong costs"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}SHIPPING_COST_CALC",
"implements": "{{P}}IF_SHIPPING_COST"
}
],
"out_of_scope": [
"{{P}}IF_SHIPPING_COST",
"{{P}}SHIPPING_COST_LEGACY"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests for the shipping cost calculation"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}SHIPPING_COST_CALC",
"file": "reference/shipping_cost_calc.clas.abap",
"description": "Correct shipping cost calculation",
"testclasses_file": "reference/shipping_cost_calc.testclasses.abap"
}
],
"craft_checks": [
"method_length",
"cyclomatic_complexity",
"no_global_variables"
]
}

View File

@@ -0,0 +1,25 @@
{
"id": "G0010",
"pool": "eval",
"object_type": "CLAS",
"category": "C",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "bundle",
"errors": [
"invalid JSON: Extra data: line 1 column 24591 (char 24590)"
]
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,173 @@
CLASS {{p}}t02_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_stock_cover.
METHODS setup.
METHODS all_ok FOR TESTING.
METHODS shortage_is_reported FOR TESTING.
METHODS stock_used_in_order FOR TESTING.
METHODS negative_stock_ignored FOR TESTING.
METHODS zero_requirement_invalid FOR TESTING.
METHODS negative_req_invalid FOR TESTING.
METHODS missing_stock_is_short FOR TESTING.
METHODS case_and_blanks_ignored FOR TESTING.
METHODS stock_items_are_summed FOR TESTING.
METHODS no_requirements FOR TESTING.
METHODS shortages_filter_sort FOR TESTING.
METHODS shortages_empty_all_ok FOR TESTING.
ENDCLASS.
CLASS {{p}}t02_hidden IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}stock_cover_checker( ).
ENDMETHOD.
METHOD all_ok.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 100 )
( material = 'MAT-B' plant = 'P1' quantity = 50 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 30 )
( material = 'MAT-B' plant = 'P1' required = 50 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 30 available = 100 shortage = 0 status = 'OK' )
( material = 'MAT-B' plant = 'P1' required = 50 available = 50 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD shortage_is_reported.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 30 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 50 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 50 available = 30 shortage = 20 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD stock_used_in_order.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 100 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 60 )
( material = 'MAT-A' plant = 'P1' required = 60 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 60 available = 100 shortage = 0 status = 'OK' )
( material = 'MAT-A' plant = 'P1' required = 60 available = 40 shortage = 20 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD negative_stock_ignored.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 100 )
( material = 'MAT-A' plant = 'P1' quantity = -40 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 100 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 100 available = 100 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD zero_requirement_invalid.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 50 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 0 )
( material = 'MAT-A' plant = 'P1' required = 50 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 0 available = 50 shortage = 0 status = 'INVALID' )
( material = 'MAT-A' plant = 'P1' required = 50 available = 50 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD negative_req_invalid.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 50 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = -10 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = -10 available = 50 shortage = 0 status = 'INVALID' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD missing_stock_is_short.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item( ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-X' plant = 'P9' required = 25 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-X' plant = 'P9' required = 25 available = 0 shortage = 25 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD case_and_blanks_ignored.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'mat-a' plant = 'p1' quantity = 80 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = ' MAT-A ' plant = 'P1 ' required = 80 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = ' MAT-A ' plant = 'P1 ' required = 80 available = 80 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD stock_items_are_summed.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 10 )
( material = 'MAT-A' plant = 'P1' quantity = 20 )
( material = 'MAT-A' plant = 'P2' quantity = 5 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 30 )
( material = 'MAT-A' plant = 'P2' required = 5 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 30 available = 30 shortage = 0 status = 'OK' )
( material = 'MAT-A' plant = 'P2' required = 5 available = 5 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD no_requirements.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 10 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement( ).
DATA(lt_act) = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( lt_act ) ).
ENDMETHOD.
METHOD shortages_filter_sort.
DATA(lt_in) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-C' plant = 'P1' required = 10 available = 0 shortage = 10 status = 'SHORT' )
( material = 'MAT-A' plant = 'P2' required = 10 available = 0 shortage = 10 status = 'SHORT' )
( material = 'MAT-B' plant = 'P1' required = 10 available = 10 shortage = 0 status = 'OK' )
( material = 'MAT-A' plant = 'P1' required = 10 available = 0 shortage = 10 status = 'SHORT' )
( material = 'MAT-D' plant = 'P1' required = 10 available = 10 shortage = 0 status = 'INVALID' ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 10 available = 0 shortage = 10 status = 'SHORT' )
( material = 'MAT-A' plant = 'P2' required = 10 available = 0 shortage = 10 status = 'SHORT' )
( material = 'MAT-C' plant = 'P1' required = 10 available = 0 shortage = 10 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals( exp = lt_exp act = mo_cut->shortages( lt_in ) ).
ENDMETHOD.
METHOD shortages_empty_all_ok.
DATA(lt_in) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 10 available = 10 shortage = 0 status = 'OK' )
( material = 'MAT-B' plant = 'P1' required = 10 available = 10 shortage = 0 status = 'INVALID' ) ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = lines( mo_cut->shortages( lt_in ) ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,128 @@
CLASS {{p}}stock_cover_checker DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES {{p}}if_stock_cover.
PRIVATE SECTION.
TYPES:
BEGIN OF ty_stock,
material TYPE {{p}}if_stock_cover=>ty_material,
plant TYPE {{p}}if_stock_cover=>ty_plant,
remaining TYPE {{p}}if_stock_cover=>ty_quantity,
END OF ty_stock.
TYPES tt_stock TYPE HASHED TABLE OF ty_stock WITH UNIQUE KEY material plant.
METHODS aggregate
IMPORTING it_stock TYPE {{p}}if_stock_cover=>tt_stock_item
RETURNING VALUE(rt_stock) TYPE tt_stock.
METHODS free_quantity
IMPORTING it_stock TYPE tt_stock
iv_material TYPE {{p}}if_stock_cover=>ty_material
iv_plant TYPE {{p}}if_stock_cover=>ty_plant
RETURNING VALUE(rv_quantity) TYPE {{p}}if_stock_cover=>ty_quantity.
METHODS consume
IMPORTING iv_material TYPE {{p}}if_stock_cover=>ty_material
iv_plant TYPE {{p}}if_stock_cover=>ty_plant
iv_quantity TYPE {{p}}if_stock_cover=>ty_quantity
CHANGING ct_stock TYPE tt_stock.
METHODS normalize_material
IMPORTING iv_material TYPE {{p}}if_stock_cover=>ty_material
RETURNING VALUE(rv_material) TYPE {{p}}if_stock_cover=>ty_material.
METHODS normalize_plant
IMPORTING iv_plant TYPE {{p}}if_stock_cover=>ty_plant
RETURNING VALUE(rv_plant) TYPE {{p}}if_stock_cover=>ty_plant.
ENDCLASS.
CLASS {{p}}stock_cover_checker IMPLEMENTATION.
METHOD {{p}}if_stock_cover~check.
DATA(lt_stock) = aggregate( it_stock ).
LOOP AT it_requirement INTO DATA(ls_requirement).
DATA(lv_free) = free_quantity( it_stock = lt_stock
iv_material = ls_requirement-material
iv_plant = ls_requirement-plant ).
DATA(ls_result) = VALUE {{p}}if_stock_cover=>ty_result(
material = ls_requirement-material
plant = ls_requirement-plant
required = ls_requirement-required
available = lv_free ).
IF ls_requirement-required <= 0.
ls_result-status = {{p}}if_stock_cover=>c_status-invalid.
ELSEIF lv_free >= ls_requirement-required.
ls_result-status = {{p}}if_stock_cover=>c_status-ok.
consume( EXPORTING iv_material = ls_requirement-material
iv_plant = ls_requirement-plant
iv_quantity = ls_requirement-required
CHANGING ct_stock = lt_stock ).
ELSE.
ls_result-status = {{p}}if_stock_cover=>c_status-short.
ls_result-shortage = ls_requirement-required - lv_free.
consume( EXPORTING iv_material = ls_requirement-material
iv_plant = ls_requirement-plant
iv_quantity = lv_free
CHANGING ct_stock = lt_stock ).
ENDIF.
APPEND ls_result TO rt_result.
ENDLOOP.
ENDMETHOD.
METHOD {{p}}if_stock_cover~shortages.
rt_shortage = it_result.
DELETE rt_shortage WHERE status <> {{p}}if_stock_cover=>c_status-short.
SORT rt_shortage BY material plant.
ENDMETHOD.
METHOD aggregate.
LOOP AT it_stock INTO DATA(ls_stock).
IF ls_stock-quantity < 0.
CONTINUE.
ENDIF.
DATA(lv_material) = normalize_material( ls_stock-material ).
DATA(lv_plant) = normalize_plant( ls_stock-plant ).
ASSIGN rt_stock[ material = lv_material plant = lv_plant ] TO FIELD-SYMBOL(<ls_stock_sum>).
IF sy-subrc = 0.
<ls_stock_sum>-remaining = <ls_stock_sum>-remaining + ls_stock-quantity.
ELSE.
INSERT VALUE #( material = lv_material
plant = lv_plant
remaining = ls_stock-quantity ) INTO TABLE rt_stock.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD free_quantity.
ASSIGN it_stock[ material = normalize_material( iv_material )
plant = normalize_plant( iv_plant ) ] TO FIELD-SYMBOL(<ls_stock>).
IF sy-subrc = 0.
rv_quantity = <ls_stock>-remaining.
ENDIF.
ENDMETHOD.
METHOD consume.
ASSIGN ct_stock[ material = normalize_material( iv_material )
plant = normalize_plant( iv_plant ) ] TO FIELD-SYMBOL(<ls_stock>).
IF sy-subrc = 0.
<ls_stock>-remaining = <ls_stock>-remaining - iv_quantity.
ENDIF.
ENDMETHOD.
METHOD normalize_material.
DATA lv_text TYPE string.
lv_text = iv_material.
REPLACE ALL OCCURRENCES OF ` ` IN lv_text WITH ``.
rv_material = to_upper( lv_text ).
ENDMETHOD.
METHOD normalize_plant.
DATA lv_text TYPE string.
lv_text = iv_plant.
REPLACE ALL OCCURRENCES OF ` ` IN lv_text WITH ``.
rv_plant = to_upper( lv_text ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,93 @@
CLASS ltc_stock_cover DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO {{p}}if_stock_cover.
METHODS setup.
METHODS covered_req_is_ok FOR TESTING.
METHODS shortage_is_reported FOR TESTING.
METHODS stock_shared_in_order FOR TESTING.
METHODS invalid_req_keeps_stock FOR TESTING.
METHODS blanks_and_case_ignored FOR TESTING.
METHODS shortages_are_sorted FOR TESTING.
ENDCLASS.
CLASS ltc_stock_cover IMPLEMENTATION.
METHOD setup.
mo_cut = NEW {{p}}stock_cover_checker( ).
ENDMETHOD.
METHOD covered_req_is_ok.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 40 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 40 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 40 available = 40 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD shortage_is_reported.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 5 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 12 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 12 available = 5 shortage = 7 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD stock_shared_in_order.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 10 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 6 )
( material = 'MAT-A' plant = 'P1' required = 6 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 6 available = 10 shortage = 0 status = 'OK' )
( material = 'MAT-A' plant = 'P1' required = 6 available = 4 shortage = 2 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD invalid_req_keeps_stock.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'MAT-A' plant = 'P1' quantity = 9 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = 'MAT-A' plant = 'P1' required = 0 )
( material = 'MAT-A' plant = 'P1' required = 9 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P1' required = 0 available = 9 shortage = 0 status = 'INVALID' )
( material = 'MAT-A' plant = 'P1' required = 9 available = 9 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD blanks_and_case_ignored.
DATA(lt_stock) = VALUE {{p}}if_stock_cover=>tt_stock_item(
( material = 'mat-a' plant = 'p1' quantity = 7 ) ).
DATA(lt_req) = VALUE {{p}}if_stock_cover=>tt_requirement(
( material = ' MAT-A ' plant = 'P1' required = 7 ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = ' MAT-A ' plant = 'P1' required = 7 available = 7 shortage = 0 status = 'OK' ) ).
cl_abap_unit_assert=>assert_equals(
exp = lt_exp
act = mo_cut->check( it_stock = lt_stock it_requirement = lt_req ) ).
ENDMETHOD.
METHOD shortages_are_sorted.
DATA(lt_in) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-B' plant = 'P1' required = 1 available = 0 shortage = 1 status = 'SHORT' )
( material = 'MAT-A' plant = 'P2' required = 1 available = 0 shortage = 1 status = 'SHORT' )
( material = 'MAT-A' plant = 'P1' required = 1 available = 1 shortage = 0 status = 'OK' ) ).
DATA(lt_exp) = VALUE {{p}}if_stock_cover=>tt_result(
( material = 'MAT-A' plant = 'P2' required = 1 available = 0 shortage = 1 status = 'SHORT' )
( material = 'MAT-B' plant = 'P1' required = 1 available = 0 shortage = 1 status = 'SHORT' ) ).
cl_abap_unit_assert=>assert_equals( exp = lt_exp act = mo_cut->shortages( lt_in ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,46 @@
INTERFACE {{p}}if_stock_cover PUBLIC.
TYPES ty_material TYPE c LENGTH 18.
TYPES ty_plant TYPE c LENGTH 4.
TYPES ty_quantity TYPE p LENGTH 13 DECIMALS 3.
TYPES ty_status TYPE c LENGTH 10.
TYPES: BEGIN OF ty_stock_item,
material TYPE ty_material,
plant TYPE ty_plant,
quantity TYPE ty_quantity,
END OF ty_stock_item.
TYPES tt_stock_item TYPE STANDARD TABLE OF ty_stock_item WITH EMPTY KEY.
TYPES: BEGIN OF ty_requirement,
material TYPE ty_material,
plant TYPE ty_plant,
required TYPE ty_quantity,
END OF ty_requirement.
TYPES tt_requirement TYPE STANDARD TABLE OF ty_requirement WITH EMPTY KEY.
TYPES: BEGIN OF ty_result,
material TYPE ty_material,
plant TYPE ty_plant,
required TYPE ty_quantity,
available TYPE ty_quantity,
shortage TYPE ty_quantity,
status TYPE ty_status,
END OF ty_result.
TYPES tt_result TYPE STANDARD TABLE OF ty_result WITH EMPTY KEY.
CONSTANTS:
BEGIN OF c_status,
ok TYPE ty_status VALUE 'OK',
short TYPE ty_status VALUE 'SHORT',
invalid TYPE ty_status VALUE 'INVALID',
END OF c_status.
METHODS check
IMPORTING it_stock TYPE tt_stock_item
it_requirement TYPE tt_requirement
RETURNING VALUE(rt_result) TYPE tt_result.
METHODS shortages
IMPORTING it_result TYPE tt_result
RETURNING VALUE(rt_shortage) TYPE tt_result.
ENDINTERFACE.

View File

@@ -0,0 +1,70 @@
# 1. Goal
A warehouse planner checks if the free stock covers the requirements of a
production run. The planner sends a list of stock items and a list of
requirements to a class. The class returns one result row for each requirement.
# 2. Open questions
None.
# 3. Context
- The interface {{P}}IF_STOCK_COVER exists in package $TMP. It is active.
- The interface contains the types TY_MATERIAL (CHAR 18), TY_PLANT (CHAR 4),
TY_QUANTITY (PACKED 13, 3) and TY_STATUS (CHAR 10).
- The interface contains the structures TY_STOCK_ITEM, TY_REQUIREMENT and
TY_RESULT and the table types TT_STOCK_ITEM, TT_REQUIREMENT and TT_RESULT.
- The interface contains the constant structure C_STATUS with the values OK,
SHORT and INVALID.
- The interface has the method CHECK. It imports a stock table and a requirement
table and returns a result table.
- The interface has the method SHORTAGES. It imports a result table and returns a
result table.
# 4. Contract
- Create the class {{P}}STOCK_COVER_CHECKER in package $TMP.
- The class is public and final.
- The class implements {{P}}IF_STOCK_COVER.
- Do not add other public methods.
- The hidden tests create the class with NEW {{P}}STOCK_COVER_CHECKER( ).
# 5. Business rules
## Stock
1. Each stock item has a material, a plant and a quantity. Ignore every stock
item whose quantity is less than zero.
2. Sum the quantities of the remaining stock items for each combination of
material and plant. This sum is the free stock of the material and plant.
3. Compare material and plant without regard to case. Remove all blanks before
you compare them.
## Requirements
4. Process the requirements in the given sequence. The result contains one row
for each requirement in the same sequence.
5. The available quantity of a requirement is the free stock of its material and
plant at the time of the requirement. If no stock exists for the material and
plant, the available quantity is 0.
6. If the required quantity is less than or equal to 0, the status is INVALID.
The requirement does not use free stock. The shortage is 0.
7. Else if the available quantity is greater than or equal to the required
quantity, the status is OK. The shortage is 0. The requirement uses the
required quantity of the free stock.
8. Else the status is SHORT. The requirement uses all free stock. The shortage is
the required quantity minus the available quantity.
## Result
9. A result row contains the material and the plant of the requirement without a
change, the required quantity, the available quantity, the shortage and the
status.
10. The method SHORTAGES returns all rows of the result table whose status is
SHORT. Sort these rows by material in ascending order and then by plant in
ascending order. The result can be empty.
# 6. Constraints
- Release target: 8.16 (SAP_BASIS 816).
- Coding standards: Clean ABAP. Method length below 40 statements. No global
variables. No comment that restates the code.
- Out of scope: do not change {{P}}IF_STOCK_COVER.
# 7. Acceptance
- The class is active and has no syntax error.
- The hidden tests pass.
- Write your own ABAP Unit tests for the class.

View File

@@ -0,0 +1,52 @@
{
"id": "G0010",
"category": "C",
"object_type": "CLAS",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 50,
"max_activations": 12
},
"seed": [
{
"type": "INTF",
"name": "{{P}}IF_STOCK_COVER",
"file": "seed/if_stock_cover.intf.abap",
"description": "Stock cover interface with types, constants and two methods"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}STOCK_COVER_CHECKER",
"implements": "{{P}}IF_STOCK_COVER"
}
],
"out_of_scope": [
"{{P}}IF_STOCK_COVER"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T02_HIDDEN",
"file": "hidden/t02_hidden.clas.abap",
"description": "T02 hidden tests for the stock cover checker"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}STOCK_COVER_CHECKER",
"file": "reference/stock_cover_checker.clas.abap",
"description": "Stock cover checker with hashed stock table",
"testclasses_file": "reference/stock_cover_checker.testclasses.abap"
}
],
"craft_checks": [
"table_kind",
"access_path",
"method_length"
]
}

View File

@@ -0,0 +1,29 @@
{
"id": "G0011",
"pool": "eval",
"object_type": "FUNC",
"category": "A",
"attempts": [
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 92.5,
"null": 0
},
{
"stage": "validate",
"oracle": 0,
"null": 0
},
{
"stage": "validate",
"oracle": 92.5,
"null": 0
}
],
"accepted": false
}

View File

@@ -0,0 +1,117 @@
CLASS {{p}}t14_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS call
IMPORTING
iv_stock TYPE decfloat34
iv_reorder_point TYPE decfloat34
iv_target_stock TYPE decfloat34
iv_pack_size TYPE decfloat34
iv_min_qty TYPE decfloat34 DEFAULT 0
EXPORTING
ev_qty TYPE decfloat34
ev_subrc TYPE sysubrc.
METHODS no_replenishment_when_stock_ok FOR TESTING.
METHODS replenish_to_target FOR TESTING.
METHODS pack_rounding_up FOR TESTING.
METHODS exact_multiple_not_rounded FOR TESTING.
METHODS minimum_quantity_applied FOR TESTING.
METHODS minimum_quantity_rounded_up FOR TESTING.
METHODS negative_minimum_is_zero FOR TESTING.
METHODS target_below_stock_is_zero FOR TESTING.
METHODS invalid_pack_size FOR TESTING.
METHODS class_calculates_directly FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_hidden IMPLEMENTATION.
METHOD call.
ev_qty = 0.
CALL FUNCTION '{{P}}REPL_CALC'
EXPORTING
iv_stock = iv_stock
iv_reorder_point = iv_reorder_point
iv_target_stock = iv_target_stock
iv_pack_size = iv_pack_size
iv_min_qty = iv_min_qty
IMPORTING
ev_qty = ev_qty
EXCEPTIONS
invalid_pack_size = 1
OTHERS = 2.
ev_subrc = sy-subrc.
ENDMETHOD.
METHOD no_replenishment_when_stock_ok.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 50 iv_reorder_point = 20 iv_target_stock = 100 iv_pack_size = 10
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = lv_qty ).
ENDMETHOD.
METHOD replenish_to_target.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 50 iv_pack_size = 12
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 48 ) act = lv_qty ).
ENDMETHOD.
METHOD pack_rounding_up.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 0 iv_reorder_point = 10 iv_target_stock = 25 iv_pack_size = 6
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 30 ) act = lv_qty ).
ENDMETHOD.
METHOD exact_multiple_not_rounded.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 0 iv_reorder_point = 10 iv_target_stock = 24 iv_pack_size = 12
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 24 ) act = lv_qty ).
ENDMETHOD.
METHOD minimum_quantity_applied.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 12 iv_pack_size = 1 iv_min_qty = 7
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 7 ) act = lv_qty ).
ENDMETHOD.
METHOD minimum_quantity_rounded_up.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 12 iv_pack_size = 5 iv_min_qty = 7
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 10 ) act = lv_qty ).
ENDMETHOD.
METHOD negative_minimum_is_zero.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 0 iv_reorder_point = 10 iv_target_stock = 10 iv_pack_size = 4 iv_min_qty = -5
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 12 ) act = lv_qty ).
ENDMETHOD.
METHOD target_below_stock_is_zero.
DATA lv_qty TYPE decfloat34.
call( EXPORTING iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 5 iv_pack_size = 4
IMPORTING ev_qty = lv_qty ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = lv_qty ).
ENDMETHOD.
METHOD invalid_pack_size.
DATA lv_rc TYPE sysubrc.
call( EXPORTING iv_stock = 0 iv_reorder_point = 10 iv_target_stock = 20 iv_pack_size = 0
IMPORTING ev_subrc = lv_rc ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_rc ).
ENDMETHOD.
METHOD class_calculates_directly.
DATA(lv_qty) = NEW {{p}}cl_repl( )->calculate(
iv_stock = 10
iv_reorder_point = 20
iv_target_stock = 50
iv_pack_size = 12
iv_min_qty = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 48 ) act = lv_qty ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,38 @@
CLASS {{p}}cl_repl DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS calculate
IMPORTING
iv_stock TYPE decfloat34
iv_reorder_point TYPE decfloat34
iv_target_stock TYPE decfloat34
iv_pack_size TYPE decfloat34
iv_min_qty TYPE decfloat34
RETURNING
VALUE(rv_qty) TYPE decfloat34.
ENDCLASS.
CLASS {{p}}cl_repl IMPLEMENTATION.
METHOD calculate.
IF iv_stock >= iv_reorder_point.
rv_qty = 0.
RETURN.
ENDIF.
DATA(lv_need) = iv_target_stock - iv_stock.
DATA(lv_min_qty) = iv_min_qty.
IF lv_min_qty < 0.
lv_min_qty = 0.
ENDIF.
IF lv_need < lv_min_qty.
lv_need = lv_min_qty.
ENDIF.
IF lv_need <= 0.
rv_qty = 0.
RETURN.
ENDIF.
rv_qty = ceil( lv_need / iv_pack_size ) * iv_pack_size.
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,24 @@
FUNCTION {{p}}repl_calc
IMPORTING
iv_stock TYPE decfloat34
iv_reorder_point TYPE decfloat34
iv_target_stock TYPE decfloat34
iv_pack_size TYPE decfloat34
iv_min_qty TYPE decfloat34
EXPORTING
ev_qty TYPE decfloat34
EXCEPTIONS
invalid_pack_size.
IF iv_pack_size <= 0.
RAISE invalid_pack_size.
ENDIF.
ev_qty = NEW {{p}}cl_repl( )->calculate(
iv_stock = iv_stock
iv_reorder_point = iv_reorder_point
iv_target_stock = iv_target_stock
iv_pack_size = iv_pack_size
iv_min_qty = iv_min_qty ).
ENDFUNCTION.

View File

@@ -0,0 +1,100 @@
CLASS {{p}}t14_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS qty_for
IMPORTING
iv_stock TYPE decfloat34
iv_reorder_point TYPE decfloat34
iv_target_stock TYPE decfloat34
iv_pack_size TYPE decfloat34
iv_min_qty TYPE decfloat34 DEFAULT 0
RETURNING
VALUE(rv_qty) TYPE decfloat34.
METHODS replenish_to_target FOR TESTING.
METHODS no_replenishment FOR TESTING.
METHODS minimum_quantity FOR TESTING.
METHODS pack_rounding FOR TESTING.
METHODS invalid_pack_size FOR TESTING.
METHODS class_direct_call FOR TESTING.
METHODS class_no_replenishment FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_test IMPLEMENTATION.
METHOD qty_for.
CALL FUNCTION '{{P}}REPL_CALC'
EXPORTING
iv_stock = iv_stock
iv_reorder_point = iv_reorder_point
iv_target_stock = iv_target_stock
iv_pack_size = iv_pack_size
iv_min_qty = iv_min_qty
IMPORTING
ev_qty = rv_qty
EXCEPTIONS
invalid_pack_size = 1
OTHERS = 2.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
ENDMETHOD.
METHOD replenish_to_target.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( 48 )
act = qty_for( iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 50 iv_pack_size = 12 ) ).
ENDMETHOD.
METHOD no_replenishment.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( 0 )
act = qty_for( iv_stock = 30 iv_reorder_point = 20 iv_target_stock = 50 iv_pack_size = 12 ) ).
ENDMETHOD.
METHOD minimum_quantity.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( 10 )
act = qty_for( iv_stock = 10 iv_reorder_point = 20 iv_target_stock = 12 iv_pack_size = 5 iv_min_qty = 7 ) ).
ENDMETHOD.
METHOD pack_rounding.
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( 30 )
act = qty_for( iv_stock = 0 iv_reorder_point = 10 iv_target_stock = 25 iv_pack_size = 6 ) ).
ENDMETHOD.
METHOD invalid_pack_size.
DATA lv_qty TYPE decfloat34.
CALL FUNCTION '{{P}}REPL_CALC'
EXPORTING
iv_stock = 0
iv_reorder_point = 10
iv_target_stock = 20
iv_pack_size = 0
iv_min_qty = 0
IMPORTING
ev_qty = lv_qty
EXCEPTIONS
invalid_pack_size = 1
OTHERS = 2.
cl_abap_unit_assert=>assert_equals( exp = 1 act = sy-subrc ).
ENDMETHOD.
METHOD class_direct_call.
DATA(lv_qty) = NEW {{p}}cl_repl( )->calculate(
iv_stock = 0
iv_reorder_point = 10
iv_target_stock = 24
iv_pack_size = 12
iv_min_qty = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 24 ) act = lv_qty ).
ENDMETHOD.
METHOD class_no_replenishment.
DATA(lv_qty) = NEW {{p}}cl_repl( )->calculate(
iv_stock = 40
iv_reorder_point = 20
iv_target_stock = 100
iv_pack_size = 10
iv_min_qty = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = lv_qty ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,9 @@
FUNCTION {{p}}repl_sample
IMPORTING
iv_bin TYPE string
EXPORTING
ev_info TYPE string.
ev_info = |Storage bin { iv_bin }|.
ENDFUNCTION.

View File

@@ -0,0 +1,66 @@
# 1. Goal
A warehouse system needs the quantity for a replenishment order of a storage
bin. The calling program calls the function module {{P}}REPL_CALC. The
calculation logic must be in the class {{P}}CL_REPL, because other programs
also use this logic.
# 2. Open questions
None.
# 3. Context
- Function group {{P}}FG_REPL exists in package $TMP.
- Function module {{P}}REPL_SAMPLE exists in this function group. It shows bin
information. Do not change it.
- The class {{P}}CL_REPL does not exist. Create it first.
# 4. Contract
## 4.1 Class {{P}}CL_REPL
- Package: $TMP.
- The class is instantiable without parameters.
- Public instance method calculate:
- importing iv_stock, type decfloat34
- importing iv_reorder_point, type decfloat34
- importing iv_target_stock, type decfloat34
- importing iv_pack_size, type decfloat34
- importing iv_min_qty, type decfloat34
- returning rv_qty, type decfloat34
- The method has no exception. Call it only with a positive pack size.
## 4.2 Function module {{P}}REPL_CALC
- Function group: {{P}}FG_REPL.
- Importing parameters:
- iv_stock, type decfloat34
- iv_reorder_point, type decfloat34
- iv_target_stock, type decfloat34
- iv_pack_size, type decfloat34
- iv_min_qty, type decfloat34
- Exporting parameters:
- ev_qty, type decfloat34
- Exceptions: invalid_pack_size.
- The function module {{P}}REPL_CALC must call the method calculate of the class
{{P}}CL_REPL. Create and activate the class before you activate the function
module.
# 5. Business rules
1. If the pack size is not positive, the function module raises the exception
INVALID_PACK_SIZE. The class method is not called in this case.
2. If the stock is greater than or equal to the reorder point, the
replenishment quantity is 0.
3. Otherwise, the requirement is the target stock minus the stock.
4. If the requirement is less than the minimum quantity, the requirement is the
minimum quantity. A minimum quantity below 0 counts as 0.
5. If the requirement is not positive, the replenishment quantity is 0.
6. Otherwise, round the requirement up to the next full multiple of the pack
size. The replenishment quantity is this multiple multiplied by the pack
size.
# 6. Constraints
- Release target: 7.58.
- Use Clean ABAP. Keep each method below 40 statements.
- Out of scope: do not change {{P}}REPL_SAMPLE. Do not create database tables.
# 7. Acceptance
- The function module and the class are active and have no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests for the function module in a global test class.

View File

@@ -0,0 +1,99 @@
{
"id": "G0011",
"category": "A",
"object_type": "FUNC",
"difficulty": 2,
"release_target": "v758",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "FUGR",
"name": "{{P}}FG_REPL",
"description": "Replenishment function group"
},
{
"type": "FUNC",
"name": "{{P}}REPL_SAMPLE",
"functionGroup": "{{P}}FG_REPL",
"file": "seed/repl_sample.func.abap",
"description": "Sample bin information module"
}
],
"contract": [
{
"type": "CLAS",
"name": "{{P}}CL_REPL"
},
{
"type": "FUNC",
"name": "{{P}}REPL_CALC",
"functionGroup": "{{P}}FG_REPL",
"params": [
{
"name": "iv_stock",
"type": "decfloat34"
},
{
"name": "iv_reorder_point",
"type": "decfloat34"
},
{
"name": "iv_target_stock",
"type": "decfloat34"
},
{
"name": "iv_pack_size",
"type": "decfloat34"
},
{
"name": "iv_min_qty",
"type": "decfloat34"
},
{
"name": "ev_qty",
"type": "decfloat34"
}
]
}
],
"out_of_scope": [
"{{P}}REPL_SAMPLE"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T14_HIDDEN",
"file": "hidden/t14_hidden.clas.abap",
"description": "T14 hidden tests"
}
],
"reference": [
{
"type": "CLAS",
"name": "{{P}}CL_REPL",
"file": "reference/cl_repl.clas.abap",
"description": "Replenishment quantity calculator"
},
{
"type": "FUNC",
"name": "{{P}}REPL_CALC",
"functionGroup": "{{P}}FG_REPL",
"file": "reference/repl_calc.func.abap",
"description": "Replenishment function module"
},
{
"type": "CLAS",
"name": "{{P}}T14_TEST",
"file": "reference/t14_test.clas.abap",
"description": "T14 own tests"
}
],
"craft_checks": [
"method_length",
"clean_abap"
]
}

View File

@@ -0,0 +1,14 @@
{
"id": "G0012",
"pool": "eval",
"object_type": "FUNC",
"category": "F",
"attempts": [
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,120 @@
CLASS {{p}}t14_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS call
IMPORTING iv_quantity TYPE decfloat34
iv_scrap TYPE decfloat34
iv_rework TYPE decfloat34 DEFAULT 0
EXPORTING ev_pct TYPE decfloat34
ev_severity TYPE string
ev_rework TYPE decfloat34
ev_subrc TYPE sysubrc.
METHODS percent_rounds_half_up FOR TESTING.
METHODS zero_scrap_is_ok FOR TESTING.
METHODS low_upper_boundary FOR TESTING.
METHODS high_lower_boundary FOR TESTING.
METHODS high_upper_boundary FOR TESTING.
METHODS critical_above_twenty FOR TESTING.
METHODS rework_unchanged_for_low FOR TESTING.
METHODS rework_set_for_high FOR TESTING.
METHODS quantity_zero_raises FOR TESTING.
METHODS scrap_negative_raises FOR TESTING.
METHODS scrap_above_quantity_raises FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_hidden IMPLEMENTATION.
METHOD call.
ev_pct = 0.
ev_severity = ``.
ev_rework = iv_rework.
CALL FUNCTION '{{P}}PRD_SCRAP'
EXPORTING
iv_quantity = iv_quantity
iv_scrap = iv_scrap
IMPORTING
ev_scrap_pct = ev_pct
ev_severity = ev_severity
CHANGING
cv_rework_qty = ev_rework
EXCEPTIONS
invalid_quantity = 1
scrap_too_high = 2
OTHERS = 3.
ev_subrc = sy-subrc.
ENDMETHOD.
METHOD percent_rounds_half_up.
call( EXPORTING iv_quantity = 800 iv_scrap = 7
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '0.9' ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `LOW` act = lv_sev ).
ENDMETHOD.
METHOD zero_scrap_is_ok.
call( EXPORTING iv_quantity = 100 iv_scrap = 0
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `OK` act = lv_sev ).
ENDMETHOD.
METHOD low_upper_boundary.
call( EXPORTING iv_quantity = 100 iv_scrap = 5
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 5 ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `LOW` act = lv_sev ).
ENDMETHOD.
METHOD high_lower_boundary.
call( EXPORTING iv_quantity = 100 iv_scrap = '5.1'
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '5.1' ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `HIGH` act = lv_sev ).
ENDMETHOD.
METHOD high_upper_boundary.
call( EXPORTING iv_quantity = 100 iv_scrap = 20
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 20 ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `HIGH` act = lv_sev ).
ENDMETHOD.
METHOD critical_above_twenty.
call( EXPORTING iv_quantity = 100 iv_scrap = '20.1'
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '20.1' ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `CRITICAL` act = lv_sev ).
ENDMETHOD.
METHOD rework_unchanged_for_low.
call( EXPORTING iv_quantity = 200 iv_scrap = 4 iv_rework = 99
IMPORTING ev_rework = DATA(lv_rework) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = `LOW` act = lv_sev ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 99 ) act = lv_rework ).
ENDMETHOD.
METHOD rework_set_for_high.
call( EXPORTING iv_quantity = 100 iv_scrap = 10 iv_rework = 99
IMPORTING ev_rework = DATA(lv_rework) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = `HIGH` act = lv_sev ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 10 ) act = lv_rework ).
ENDMETHOD.
METHOD quantity_zero_raises.
call( EXPORTING iv_quantity = 0 iv_scrap = 0
IMPORTING ev_subrc = DATA(lv_rc) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_rc ).
ENDMETHOD.
METHOD scrap_negative_raises.
call( EXPORTING iv_quantity = 100 iv_scrap = -1
IMPORTING ev_subrc = DATA(lv_rc) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_rc ).
ENDMETHOD.
METHOD scrap_above_quantity_raises.
call( EXPORTING iv_quantity = 100 iv_scrap = 101
IMPORTING ev_subrc = DATA(lv_rc) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lv_rc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,41 @@
FUNCTION {{p}}prd_scrap
IMPORTING
iv_quantity TYPE decfloat34
iv_scrap TYPE decfloat34
EXPORTING
ev_scrap_pct TYPE decfloat34
ev_severity TYPE string
CHANGING
cv_rework_qty TYPE decfloat34
EXCEPTIONS
invalid_quantity
scrap_too_high.
IF iv_quantity <= 0 OR iv_scrap < 0.
RAISE invalid_quantity.
ENDIF.
IF iv_scrap > iv_quantity.
RAISE scrap_too_high.
ENDIF.
ev_scrap_pct = round( val = iv_scrap / iv_quantity * 100
dec = 1
mode = cl_abap_math=>round_half_up ).
IF ev_scrap_pct = 0.
ev_severity = 'OK'.
ELSEIF ev_scrap_pct <= 5.
ev_severity = 'LOW'.
ELSEIF ev_scrap_pct <= 20.
ev_severity = 'HIGH'.
ELSE.
ev_severity = 'CRITICAL'.
ENDIF.
CASE ev_severity.
WHEN 'HIGH' OR 'CRITICAL'.
cv_rework_qty = iv_scrap.
ENDCASE.
ENDFUNCTION.

View File

@@ -0,0 +1,90 @@
CLASS {{p}}t14_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS result_for
IMPORTING iv_quantity TYPE decfloat34
iv_scrap TYPE decfloat34
iv_rework TYPE decfloat34 DEFAULT 0
EXPORTING ev_pct TYPE decfloat34
ev_severity TYPE string
ev_rework TYPE decfloat34.
METHODS percent_and_severity FOR TESTING.
METHODS severity_bands FOR TESTING.
METHODS rework_quantity FOR TESTING.
METHODS invalid_inputs FOR TESTING.
METHODS scrap_too_high FOR TESTING.
ENDCLASS.
CLASS {{p}}t14_test IMPLEMENTATION.
METHOD result_for.
ev_rework = iv_rework.
CALL FUNCTION '{{P}}PRD_SCRAP'
EXPORTING
iv_quantity = iv_quantity
iv_scrap = iv_scrap
IMPORTING
ev_scrap_pct = ev_pct
ev_severity = ev_severity
CHANGING
cv_rework_qty = ev_rework
EXCEPTIONS
invalid_quantity = 1
scrap_too_high = 2
OTHERS = 3.
cl_abap_unit_assert=>assert_subrc( exp = 0 ).
ENDMETHOD.
METHOD percent_and_severity.
result_for( EXPORTING iv_quantity = 800 iv_scrap = 7
IMPORTING ev_pct = DATA(lv_pct) ev_severity = DATA(lv_sev) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '0.9' ) act = lv_pct ).
cl_abap_unit_assert=>assert_equals( exp = `LOW` act = lv_sev ).
ENDMETHOD.
METHOD severity_bands.
result_for( EXPORTING iv_quantity = 100 iv_scrap = 0
IMPORTING ev_severity = DATA(lv_ok) ).
cl_abap_unit_assert=>assert_equals( exp = `OK` act = lv_ok ).
result_for( EXPORTING iv_quantity = 100 iv_scrap = 5
IMPORTING ev_severity = DATA(lv_low) ).
cl_abap_unit_assert=>assert_equals( exp = `LOW` act = lv_low ).
result_for( EXPORTING iv_quantity = 100 iv_scrap = 20
IMPORTING ev_severity = DATA(lv_high) ).
cl_abap_unit_assert=>assert_equals( exp = `HIGH` act = lv_high ).
result_for( EXPORTING iv_quantity = 100 iv_scrap = 21
IMPORTING ev_severity = DATA(lv_crit) ).
cl_abap_unit_assert=>assert_equals( exp = `CRITICAL` act = lv_crit ).
ENDMETHOD.
METHOD rework_quantity.
result_for( EXPORTING iv_quantity = 100 iv_scrap = 4 iv_rework = 99
IMPORTING ev_rework = DATA(lv_keep) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 99 ) act = lv_keep ).
result_for( EXPORTING iv_quantity = 100 iv_scrap = 10
IMPORTING ev_rework = DATA(lv_set) ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 10 ) act = lv_set ).
ENDMETHOD.
METHOD invalid_inputs.
DATA lv_pct TYPE decfloat34.
DATA lv_rework TYPE decfloat34.
CALL FUNCTION '{{P}}PRD_SCRAP'
EXPORTING iv_quantity = 0 iv_scrap = 0
IMPORTING ev_scrap_pct = lv_pct
CHANGING cv_rework_qty = lv_rework
EXCEPTIONS invalid_quantity = 1 OTHERS = 2.
cl_abap_unit_assert=>assert_equals( exp = 1 act = sy-subrc ).
ENDMETHOD.
METHOD scrap_too_high.
DATA lv_pct TYPE decfloat34.
DATA lv_rework TYPE decfloat34.
CALL FUNCTION '{{P}}PRD_SCRAP'
EXPORTING iv_quantity = 100 iv_scrap = 101
IMPORTING ev_scrap_pct = lv_pct
CHANGING cv_rework_qty = lv_rework
EXCEPTIONS scrap_too_high = 2 OTHERS = 1.
cl_abap_unit_assert=>assert_equals( exp = 2 act = sy-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,18 @@
FUNCTION {{p}}prd_sample
IMPORTING
iv_quantity TYPE decfloat34
iv_scrap TYPE decfloat34
EXPORTING
ev_scrap_pct TYPE decfloat34
ev_severity TYPE string
CHANGING
cv_rework_qty TYPE decfloat34
EXCEPTIONS
invalid_quantity
scrap_too_high.
" Sample confirmation module. All confirmation modules use this interface.
ev_scrap_pct = 0.
ev_severity = 'OK'.
ENDFUNCTION.

View File

@@ -0,0 +1,46 @@
# 1. Goal
Evaluate a production confirmation. Calculate the scrap percentage and the
severity of the scrap. The confirmation program calls one module for each
confirmation. The new module must have the same interface as the sample module,
because the confirmation program calls all modules in the same way.
# 2. Open questions
None.
# 3. Context
- Function group {{P}}FG_PROD exists in package $TMP.
- Function module {{P}}PRD_SAMPLE exists in this group. It is the sample
confirmation module. Its interface is the interface that all confirmation
modules must use.
# 4. Contract
- Create the function module {{P}}PRD_SCRAP in function group {{P}}FG_PROD.
- Use exactly the interface of {{P}}PRD_SAMPLE: the same parameter names, kinds,
types, and exceptions.
# 5. Business rules
1. If the confirmed quantity is less than or equal to 0, raise the exception
INVALID_QUANTITY.
2. If the scrap quantity is less than 0, raise the exception INVALID_QUANTITY.
3. If the scrap quantity is greater than the confirmed quantity, raise the
exception SCRAP_TOO_HIGH.
4. The scrap percentage is the scrap quantity divided by the confirmed quantity,
multiplied by 100. Round the result half up to 1 decimal. Write the result to
the scrap percentage parameter.
5. Set the severity from the rounded scrap percentage of rule 4:
- equal to 0: `OK`
- greater than 0 and less than or equal to 5: `LOW`
- greater than 5 and less than or equal to 20: `HIGH`
- greater than 20: `CRITICAL`
6. If the severity is `HIGH` or `CRITICAL`, set the rework quantity to the scrap
quantity. In all other cases, do not change the rework quantity.
# 6. Constraints
- Release target: 7.58.
- Coding standards: Clean ABAP. Keep every method below 40 statements.
- Out of scope: do not change {{P}}PRD_SAMPLE.
# 7. Acceptance
- The function module is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests for the function module in a global test class.

View File

@@ -0,0 +1,84 @@
{
"id": "G0012",
"category": "F",
"object_type": "FUNC",
"difficulty": 2,
"release_target": "v758",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "FUGR",
"name": "{{P}}FG_PROD",
"description": "Production confirmation modules"
},
{
"type": "FUNC",
"name": "{{P}}PRD_SAMPLE",
"functionGroup": "{{P}}FG_PROD",
"file": "seed/prd_sample.func.abap",
"description": "Sample confirmation module"
}
],
"contract": [
{
"type": "FUNC",
"name": "{{P}}PRD_SCRAP",
"functionGroup": "{{P}}FG_PROD",
"params": [
{
"name": "iv_quantity",
"type": "decfloat34"
},
{
"name": "iv_scrap",
"type": "decfloat34"
},
{
"name": "ev_scrap_pct",
"type": "decfloat34"
},
{
"name": "ev_severity",
"type": "string"
},
{
"name": "cv_rework_qty",
"type": "decfloat34"
}
]
}
],
"out_of_scope": [
"{{P}}PRD_SAMPLE"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T14_HIDDEN",
"file": "hidden/t14_hidden.clas.abap",
"description": "T14 hidden tests"
}
],
"reference": [
{
"type": "FUNC",
"name": "{{P}}PRD_SCRAP",
"functionGroup": "{{P}}FG_PROD",
"file": "reference/prd_scrap.func.abap",
"description": "Scrap evaluation module"
},
{
"type": "CLAS",
"name": "{{P}}T14_TEST",
"file": "reference/t14_test.clas.abap",
"description": "T14 own tests"
}
],
"craft_checks": [
"method_length"
]
}

View File

@@ -0,0 +1,26 @@
{
"id": "G0013",
"pool": "eval",
"object_type": "FUNC",
"category": "I",
"attempts": [
{
"stage": "validate",
"oracle": 70.5,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
},
{
"stage": "manual_fix",
"by": "Claude review 2026-10-03",
"change": "Removed own test class REPL_TEST from the contract and from the spec acceptance (own tests are the model's choice). REPL_TEST stays in the reference as own tests.",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,108 @@
CLASS {{p}}t21_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES: BEGIN OF ty_result,
qty TYPE decfloat34,
action TYPE string,
subrc TYPE sysubrc,
END OF ty_result.
METHODS call
IMPORTING iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
RETURNING VALUE(rs_result) TYPE ty_result.
METHODS exact_multiple_lot FOR TESTING.
METHODS exact_multiple_single_lot FOR TESTING.
METHODS exact_multiple_with_minimum FOR TESTING.
METHODS partial_lot_rounds_up FOR TESTING.
METHODS stock_above_safety FOR TESTING.
METHODS stock_equals_safety FOR TESTING.
METHODS no_lot_size_no_rounding FOR TESTING.
METHODS minimum_order_applies FOR TESTING.
METHODS minimum_order_keeps_quantity FOR TESTING.
METHODS negative_lot_size FOR TESTING.
METHODS negative_min_order FOR TESTING.
ENDCLASS.
CLASS {{p}}t21_hidden IMPLEMENTATION.
METHOD call.
CALL FUNCTION '{{P}}REPL_QTY'
EXPORTING
iv_stock = iv_stock
iv_safety = iv_safety
iv_lot_size = iv_lot_size
iv_min_order = iv_min_order
IMPORTING
ev_order_qty = rs_result-qty
ev_action = rs_result-action
EXCEPTIONS
invalid_input = 1
OTHERS = 2.
rs_result-subrc = sy-subrc.
ENDMETHOD.
METHOD exact_multiple_lot.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = ls_result-subrc ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 100 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `ORDER` act = ls_result-action ).
ENDMETHOD.
METHOD exact_multiple_single_lot.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 50 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 0 act = ls_result-subrc ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 50 ) act = ls_result-qty ).
ENDMETHOD.
METHOD exact_multiple_with_minimum.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = 50 iv_min_order = 120 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 120 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `ORDER` act = ls_result-action ).
ENDMETHOD.
METHOD partial_lot_rounds_up.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 110 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 150 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `ORDER` act = ls_result-action ).
ENDMETHOD.
METHOD stock_above_safety.
DATA(ls_result) = call( iv_stock = 200 iv_safety = 100 iv_lot_size = 50 iv_min_order = 50 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `NONE` act = ls_result-action ).
ENDMETHOD.
METHOD stock_equals_safety.
DATA(ls_result) = call( iv_stock = 100 iv_safety = 100 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `NONE` act = ls_result-action ).
ENDMETHOD.
METHOD no_lot_size_no_rounding.
DATA(ls_result) = call( iv_stock = 0 iv_safety = '37.5' iv_lot_size = 0 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( '37.5' ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `ORDER` act = ls_result-action ).
ENDMETHOD.
METHOD minimum_order_applies.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 10 iv_lot_size = 0 iv_min_order = 25 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 25 ) act = ls_result-qty ).
ENDMETHOD.
METHOD minimum_order_keeps_quantity.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = 0 iv_min_order = 25 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 100 ) act = ls_result-qty ).
ENDMETHOD.
METHOD negative_lot_size.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = -1 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = ls_result-subrc ).
ENDMETHOD.
METHOD negative_min_order.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = 0 iv_min_order = -1 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = ls_result-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,38 @@
FUNCTION {{p}}repl_qty
IMPORTING
iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
EXPORTING
ev_order_qty TYPE decfloat34
ev_action TYPE string
EXCEPTIONS
invalid_input.
IF iv_lot_size < 0 OR iv_min_order < 0.
RAISE invalid_input.
ENDIF.
DATA lv_required TYPE decfloat34.
lv_required = iv_safety - iv_stock.
IF lv_required <= 0.
ev_order_qty = 0.
ev_action = 'NONE'.
RETURN.
ENDIF.
IF iv_lot_size > 0.
ev_order_qty = ceil( lv_required / iv_lot_size ) * iv_lot_size.
ELSE.
ev_order_qty = lv_required.
ENDIF.
IF ev_order_qty < iv_min_order.
ev_order_qty = iv_min_order.
ENDIF.
ev_action = 'ORDER'.
ENDFUNCTION.

View File

@@ -0,0 +1,65 @@
CLASS {{p}}repl_test DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES: BEGIN OF ty_result,
qty TYPE decfloat34,
action TYPE string,
subrc TYPE sysubrc,
END OF ty_result.
METHODS call
IMPORTING iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
RETURNING VALUE(rs_result) TYPE ty_result.
METHODS exact_multiple FOR TESTING.
METHODS partial_lot FOR TESTING.
METHODS no_order FOR TESTING.
METHODS minimum_order FOR TESTING.
METHODS invalid_input FOR TESTING.
ENDCLASS.
CLASS {{p}}repl_test IMPLEMENTATION.
METHOD call.
CALL FUNCTION '{{P}}REPL_QTY'
EXPORTING
iv_stock = iv_stock
iv_safety = iv_safety
iv_lot_size = iv_lot_size
iv_min_order = iv_min_order
IMPORTING
ev_order_qty = rs_result-qty
ev_action = rs_result-action
EXCEPTIONS
invalid_input = 1
OTHERS = 2.
rs_result-subrc = sy-subrc.
ENDMETHOD.
METHOD exact_multiple.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 100 ) act = ls_result-qty ).
ENDMETHOD.
METHOD partial_lot.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 101 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 150 ) act = ls_result-qty ).
ENDMETHOD.
METHOD no_order.
DATA(ls_result) = call( iv_stock = 300 iv_safety = 100 iv_lot_size = 50 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 0 ) act = ls_result-qty ).
cl_abap_unit_assert=>assert_equals( exp = `NONE` act = ls_result-action ).
ENDMETHOD.
METHOD minimum_order.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 5 iv_lot_size = 0 iv_min_order = 20 ).
cl_abap_unit_assert=>assert_equals( exp = CONV decfloat34( 20 ) act = ls_result-qty ).
ENDMETHOD.
METHOD invalid_input.
DATA(ls_result) = call( iv_stock = 0 iv_safety = 100 iv_lot_size = -5 iv_min_order = 0 ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = ls_result-subrc ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,38 @@
FUNCTION {{p}}repl_qty
IMPORTING
iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
EXPORTING
ev_order_qty TYPE decfloat34
ev_action TYPE string
EXCEPTIONS
invalid_input.
IF iv_lot_size < 0.
RAISE invalid_input.
ENDIF.
DATA lv_required TYPE decfloat34.
lv_required = iv_safety - iv_stock.
IF lv_required <= 0.
ev_order_qty = 0.
ev_action = 'NONE'.
RETURN.
ENDIF.
IF iv_lot_size > 0.
ev_order_qty = ( floor( lv_required / iv_lot_size ) + 1 ) * iv_lot_size.
ELSE.
ev_order_qty = lv_required.
ENDIF.
IF ev_order_qty < iv_min_order.
ev_order_qty = iv_min_order.
ENDIF.
ev_action = 'ORDER'.
ENDFUNCTION.

View File

@@ -0,0 +1,38 @@
FUNCTION {{p}}repl_qty_old
IMPORTING
iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
EXPORTING
ev_order_qty TYPE decfloat34
ev_action TYPE string
EXCEPTIONS
invalid_input.
IF iv_lot_size < 0.
RAISE invalid_input.
ENDIF.
DATA lv_required TYPE decfloat34.
lv_required = iv_safety - iv_stock.
IF lv_required <= 0.
ev_order_qty = 0.
ev_action = 'NONE'.
RETURN.
ENDIF.
IF iv_lot_size > 0.
ev_order_qty = ( floor( lv_required / iv_lot_size ) + 1 ) * iv_lot_size.
ELSE.
ev_order_qty = lv_required.
ENDIF.
IF ev_order_qty < iv_min_order.
ev_order_qty = iv_min_order.
ENDIF.
ev_action = 'ORDER'.
ENDFUNCTION.

View File

@@ -0,0 +1,48 @@
# 1. Goal
The function module {{P}}REPL_QTY_OLD calculates the replenishment order quantity for a material. Users report two defects:
- When the required quantity is exactly a multiple of the lot size, the module orders one lot too many.
- The module accepts a negative minimum order quantity. It must report an error instead.
The module {{P}}REPL_QTY_OLD is used by a released interface and must not change. Deliver the corrected calculation in the new function module {{P}}REPL_QTY.
# 2. Open questions
None.
# 3. Context
- Function group {{P}}FG_REPL exists in package $TMP.
- Function module {{P}}REPL_QTY_OLD exists in this group. It is the module in production. It has the two defects above.
- The replenishment program will call {{P}}REPL_QTY.
# 4. Contract
Create the function module {{P}}REPL_QTY in function group {{P}}FG_REPL with this interface:
IMPORTING
iv_stock TYPE decfloat34
iv_safety TYPE decfloat34
iv_lot_size TYPE decfloat34
iv_min_order TYPE decfloat34
EXPORTING
ev_order_qty TYPE decfloat34
ev_action TYPE string
EXCEPTIONS
invalid_input
ev_action contains the value 'ORDER' or the value 'NONE'.
# 5. Business rules
1. If iv_lot_size is less than 0, or if iv_min_order is less than 0, raise the exception INVALID_INPUT.
2. The required quantity is iv_safety minus iv_stock.
3. If the required quantity is less than or equal to 0, ev_order_qty is 0 and ev_action is 'NONE'. Rules 4 to 6 do not apply then.
4. If iv_lot_size is greater than 0, round the required quantity up to the next multiple of iv_lot_size. A required quantity that is already a multiple of iv_lot_size does not change.
5. If iv_lot_size is 0, do not round.
6. If ev_order_qty is less than iv_min_order, set ev_order_qty to iv_min_order.
7. If ev_order_qty is greater than 0, ev_action is 'ORDER'. Otherwise ev_action is 'NONE'.
# 6. Constraints
- Release target: 8.16.
- Coding standards: Clean ABAP. Keep methods below 40 statements.
- Out of scope: do not change the function group {{P}}FG_REPL. Do not change {{P}}REPL_QTY_OLD.
# 7. Acceptance
- The function module {{P}}REPL_QTY is active and has no syntax error.
- The hidden tests pass.
- Write ABAP Unit tests for the function module in a global test class.

View File

@@ -0,0 +1,89 @@
{
"id": "G0013",
"category": "I",
"object_type": "FUNC",
"difficulty": 2,
"release_target": "v816",
"expected_outcome": "implement",
"budget": {
"max_tool_calls": 40,
"max_activations": 10
},
"seed": [
{
"type": "FUGR",
"name": "{{P}}FG_REPL",
"description": "Replenishment function group"
},
{
"type": "FUNC",
"name": "{{P}}REPL_QTY_OLD",
"functionGroup": "{{P}}FG_REPL",
"file": "seed/repl_qty_old.func.abap",
"description": "Faulty replenishment order quantity module"
}
],
"contract": [
{
"type": "FUNC",
"name": "{{P}}REPL_QTY",
"functionGroup": "{{P}}FG_REPL",
"params": [
{
"name": "iv_stock",
"type": "decfloat34"
},
{
"name": "iv_safety",
"type": "decfloat34"
},
{
"name": "iv_lot_size",
"type": "decfloat34"
},
{
"name": "iv_min_order",
"type": "decfloat34"
},
{
"name": "ev_order_qty",
"type": "decfloat34"
},
{
"name": "ev_action",
"type": "string"
}
]
}
],
"out_of_scope": [
"{{P}}FG_REPL",
"{{P}}REPL_QTY_OLD"
],
"hidden_tests": [
{
"type": "CLAS",
"name": "{{P}}T21_HIDDEN",
"file": "hidden/t21_hidden.clas.abap",
"description": "T21 hidden tests"
}
],
"reference": [
{
"type": "FUNC",
"name": "{{P}}REPL_QTY",
"functionGroup": "{{P}}FG_REPL",
"file": "reference/repl_qty.func.abap",
"description": "Corrected replenishment order quantity module"
},
{
"type": "CLAS",
"name": "{{P}}REPL_TEST",
"file": "reference/t21_test.clas.abap",
"description": "Own tests of the reference (not contract)"
}
],
"craft_checks": [
"method_length"
]
}

View File

@@ -0,0 +1,19 @@
{
"id": "G0014",
"pool": "eval",
"object_type": "PROG",
"category": "B",
"attempts": [
{
"stage": "validate",
"oracle": 93.3,
"null": 0
},
{
"stage": "validate",
"oracle": 100.0,
"null": 0
}
],
"accepted": true
}

View File

@@ -0,0 +1,112 @@
CLASS {{p}}t15_hidden DEFINITION PUBLIC FINAL CREATE PUBLIC
FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
TYPES tt_plant TYPE RANGE OF {{p}}movmt-plant.
METHODS run
IMPORTING iv_from TYPE d
iv_to TYPE d
it_plant TYPE tt_plant OPTIONAL
RETURNING VALUE(rr_data) TYPE REF TO data.
METHODS value_of
IMPORTING ir_data TYPE REF TO data
iv_row TYPE i
iv_col TYPE string
RETURNING VALUE(rv_value) TYPE string.
METHODS lines_of
IMPORTING ir_data TYPE REF TO data
RETURNING VALUE(rv_lines) TYPE i.
METHODS all_materials_in_range FOR TESTING.
METHODS receipt_and_issue_split FOR TESTING.
METHODS sorted_by_balance FOR TESTING.
METHODS plant_filter FOR TESTING.
METHODS date_range_filter FOR TESTING.
METHODS ignored_move_types FOR TESTING.
ENDCLASS.
CLASS {{p}}t15_hidden IMPLEMENTATION.
METHOD run.
cl_salv_bs_runtime_info=>set( display = abap_false metadata = abap_false data = abap_true ).
SUBMIT {{p}}stock_bal
WITH p_from = iv_from
WITH p_to = iv_to
WITH s_plant IN it_plant
AND RETURN.
TRY.
cl_salv_bs_runtime_info=>get_data_ref( IMPORTING r_data = rr_data ).
CATCH cx_salv_bs_sc_runtime_info.
ENDTRY.
cl_salv_bs_runtime_info=>clear_all( ).
ENDMETHOD.
METHOD lines_of.
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
IF ir_data IS BOUND.
ASSIGN ir_data->* TO <lt_data>.
rv_lines = lines( <lt_data> ).
ENDIF.
ENDMETHOD.
METHOD value_of.
FIELD-SYMBOLS <lt_data> TYPE STANDARD TABLE.
ASSIGN ir_data->* TO <lt_data>.
ASSIGN <lt_data>[ iv_row ] TO FIELD-SYMBOL(<ls_line>).
ASSIGN COMPONENT iv_col OF STRUCTURE <ls_line> TO FIELD-SYMBOL(<lv_value>).
cl_abap_unit_assert=>assert_subrc( exp = 0 msg = |Column { iv_col } missing| ).
rv_value = condense( CONV string( <lv_value> ) ).
" The ALV data can show a negative number with a trailing sign.
DATA(lv_length) = strlen( rv_value ).
IF lv_length > 1 AND substring( val = rv_value off = lv_length - 1 len = 1 ) = '-'.
rv_value = '-' && substring( val = rv_value off = 0 len = lv_length - 1 ).
ENDIF.
ENDMETHOD.
METHOD all_materials_in_range.
DATA(lr_data) = run( iv_from = '20260101' iv_to = '20260131' ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-001` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `120` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `BALANCE` ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-002` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `-20` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `BALANCE` ) ).
ENDMETHOD.
METHOD receipt_and_issue_split.
DATA(lr_data) = run( iv_from = '20260101' iv_to = '20260131' ).
cl_abap_unit_assert=>assert_equals( exp = `150` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `RECEIPT_QTY` ) ).
cl_abap_unit_assert=>assert_equals( exp = `30` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `ISSUE_QTY` ) ).
cl_abap_unit_assert=>assert_equals( exp = `40` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `RECEIPT_QTY` ) ).
cl_abap_unit_assert=>assert_equals( exp = `60` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `ISSUE_QTY` ) ).
ENDMETHOD.
METHOD sorted_by_balance.
DATA(lr_data) = run( iv_from = '20251201' iv_to = '20260131' ).
cl_abap_unit_assert=>assert_equals( exp = 3 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-001` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-004` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-002` act = value_of( ir_data = lr_data iv_row = 3 iv_col = `MATERIAL` ) ).
ENDMETHOD.
METHOD plant_filter.
DATA(lr_data) = run( iv_from = '20260101' iv_to = '20260131'
it_plant = VALUE #( ( sign = 'I' option = 'EQ' low = '1000' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-001` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `70` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `BALANCE` ) ).
ENDMETHOD.
METHOD date_range_filter.
DATA(lr_data) = run( iv_from = '20260101' iv_to = '20260109' ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `100` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `BALANCE` ) ).
cl_abap_unit_assert=>assert_equals( exp = `40` act = value_of( ir_data = lr_data iv_row = 2 iv_col = `BALANCE` ) ).
ENDMETHOD.
METHOD ignored_move_types.
DATA(lr_data) = run( iv_from = '20260101' iv_to = '20260131'
it_plant = VALUE #( ( sign = 'I' option = 'EQ' low = '2000' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines_of( lr_data ) ).
cl_abap_unit_assert=>assert_equals( exp = `MAT-001` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `MATERIAL` ) ).
cl_abap_unit_assert=>assert_equals( exp = `50` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `RECEIPT_QTY` ) ).
cl_abap_unit_assert=>assert_equals( exp = `0` act = value_of( ir_data = lr_data iv_row = 1 iv_col = `ISSUE_QTY` ) ).
ENDMETHOD.
ENDCLASS.

View File

@@ -0,0 +1,127 @@
REPORT {{p}}stock_bal.
DATA gv_plant TYPE {{p}}movmt-plant.
PARAMETERS p_from TYPE d OBLIGATORY.
PARAMETERS p_to TYPE d OBLIGATORY.
SELECT-OPTIONS s_plant FOR gv_plant.
CLASS lcl_report DEFINITION FINAL.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_line,
material TYPE {{p}}movmt-material,
receipt_qty TYPE i,
issue_qty TYPE i,
balance TYPE i,
END OF ty_line,
tt_line TYPE STANDARD TABLE OF ty_line WITH EMPTY KEY,
tt_plant TYPE RANGE OF {{p}}movmt-plant.
METHODS select_balances
IMPORTING iv_from TYPE d
iv_to TYPE d
it_plant TYPE tt_plant
RETURNING VALUE(rt_lines) TYPE tt_line.
METHODS display
CHANGING ct_lines TYPE tt_line.
ENDCLASS.
CLASS lcl_report IMPLEMENTATION.
METHOD select_balances.
SELECT material,
SUM( CASE WHEN move_type = '101' OR move_type = '201'
THEN quantity
ELSE 0 END ) AS receipt_qty,
SUM( CASE WHEN move_type = '261' OR move_type = '262'
THEN quantity
ELSE 0 END ) AS issue_qty
FROM {{p}}movmt
WHERE move_date >= @iv_from
AND move_date <= @iv_to
AND plant IN @it_plant
AND move_type IN ( '101', '201', '261', '262' )
GROUP BY material
INTO CORRESPONDING FIELDS OF TABLE @rt_lines.
LOOP AT rt_lines ASSIGNING FIELD-SYMBOL(<ls_line>).
<ls_line>-balance = <ls_line>-receipt_qty - <ls_line>-issue_qty.
ENDLOOP.
SORT rt_lines BY balance DESCENDING.
ENDMETHOD.
METHOD display.
TRY.
cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
CHANGING t_table = ct_lines ).
lo_alv->display( ).
CATCH cx_salv_msg INTO DATA(lx_error).
MESSAGE lx_error TYPE 'I' DISPLAY LIKE 'E'.
ENDTRY.
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
DATA(go_report) = NEW lcl_report( ).
DATA(gt_lines) = go_report->select_balances( iv_from = p_from
iv_to = p_to
it_plant = s_plant[] ).
go_report->display( CHANGING ct_lines = gt_lines ).
CLASS ltc_report DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
CLASS-DATA go_env TYPE REF TO if_osql_test_environment.
CLASS-METHODS class_setup.
CLASS-METHODS class_teardown.
METHODS balances_per_material FOR TESTING.
METHODS plant_filter FOR TESTING.
METHODS negative_balance FOR TESTING.
ENDCLASS.
CLASS ltc_report IMPLEMENTATION.
METHOD class_setup.
go_env = cl_osql_test_environment=>create( i_dependency_list = VALUE #( ( '{{P}}MOVMT' ) ) ).
DATA lt_movmt TYPE STANDARD TABLE OF {{p}}movmt WITH EMPTY KEY.
lt_movmt = VALUE #(
( move_id = 1 material = 'MAT-A' plant = '1000' move_date = '20260105' move_type = '101' quantity = 100 )
( move_id = 2 material = 'MAT-A' plant = '1000' move_date = '20260110' move_type = '261' quantity = 30 )
( move_id = 3 material = 'MAT-B' plant = '2000' move_date = '20260112' move_type = '201' quantity = 40 )
( move_id = 4 material = 'MAT-B' plant = '2000' move_date = '20260113' move_type = '262' quantity = 90 )
( move_id = 5 material = 'MAT-C' plant = '2000' move_date = '20260112' move_type = '999' quantity = 500 ) ).
go_env->insert_test_data( lt_movmt ).
ENDMETHOD.
METHOD class_teardown.
go_env->destroy( ).
ENDMETHOD.
METHOD balances_per_material.
DATA(lt_lines) = NEW lcl_report( )->select_balances(
iv_from = '20260101' iv_to = '20260131' it_plant = VALUE #( ) ).
cl_abap_unit_assert=>assert_equals( exp = 2 act = lines( lt_lines ) ).
cl_abap_unit_assert=>assert_equals( exp = 'MAT-A' act = lt_lines[ 1 ]-material ).
cl_abap_unit_assert=>assert_equals( exp = 70 act = lt_lines[ 1 ]-balance ).
cl_abap_unit_assert=>assert_equals( exp = 'MAT-B' act = lt_lines[ 2 ]-material ).
cl_abap_unit_assert=>assert_equals( exp = -50 act = lt_lines[ 2 ]-balance ).
ENDMETHOD.
METHOD plant_filter.
DATA(lt_lines) = NEW lcl_report( )->select_balances(
iv_from = '20260101'
iv_to = '20260131'
it_plant = VALUE #( ( sign = 'I' option = 'EQ' low = '1000' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
cl_abap_unit_assert=>assert_equals( exp = 'MAT-A' act = lt_lines[ 1 ]-material ).
cl_abap_unit_assert=>assert_equals( exp = 30 act = lt_lines[ 1 ]-issue_qty ).
ENDMETHOD.
METHOD negative_balance.
DATA(lt_lines) = NEW lcl_report( )->select_balances(
iv_from = '20260101'
iv_to = '20260131'
it_plant = VALUE #( ( sign = 'I' option = 'EQ' low = '2000' ) ) ).
cl_abap_unit_assert=>assert_equals( exp = 1 act = lines( lt_lines ) ).
cl_abap_unit_assert=>assert_equals( exp = 'MAT-B' act = lt_lines[ 1 ]-material ).
cl_abap_unit_assert=>assert_equals( exp = -50 act = lt_lines[ 1 ]-balance ).
ENDMETHOD.
ENDCLASS.

Some files were not shown because too many files have changed in this diff Show More