SKILL: Uporządkowanie skills domenowych - podział na mniejsze pliki i wspólna numeracja
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# CRM01 — Wyszukiwanie i identyfikacja
|
||||
|
||||
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md).
|
||||
|
||||
### CRM-W1 — Wyszukiwanie kontrahenta
|
||||
|
||||
**Cel:** odnaleźć istniejącego kontrahenta po wybranym kluczu, zanim zaczniemy go modyfikować lub
|
||||
zanim utworzymy nowy rekord.
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Klucz | Uwaga |
|
||||
|---|---|---|
|
||||
| Po kodzie | `Kod` | indeks `WgKodu`, klucz unikalny — zwraca pojedynczy rekord |
|
||||
| Po nazwie / fragmencie | `Nazwa` | indeks `WgNazwy` (nieunikalny) lub `SubTable[pattern]` |
|
||||
| Po NIP / EU VAT | `NIP`, `EuVAT` | normalizacja: `Nip.Flat` / `EuVat.Flat` przed porównaniem |
|
||||
| Po adresie | `Adres.*` | miejscowość, kod pocztowy, ulica |
|
||||
| Po PESEL / REGON / KRS | `PESEL`, `REGON`, `KRS` | osoby fizyczne / podmioty |
|
||||
| Dedup przed dodaniem | `NIP` | sprawdzenie, czy podmiot już istnieje |
|
||||
| Kontrahent incydentalny | `JestIncydentalny` | systemowy rekord (`Kontrahent.INCYDENTALNY`) |
|
||||
|
||||
**Pola i typy:** `Kod: string`, `NIP: string`, `EuVAT: string`, `Nazwa: string`,
|
||||
`Adres: Soneta.Core.Adres`, `PESEL/REGON/KRS: string`, `JestIncydentalny: bool`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var crm = session.GetCRM();
|
||||
|
||||
// 1. Po kodzie — klucz unikalny, zwraca pojedynczy rekord lub null
|
||||
Kontrahent poKodzie = crm.Kontrahenci.WgKodu["ABC"];
|
||||
|
||||
// 2. Po nazwie — indeks nieunikalny, zwraca zbiór; bierzemy pierwszy
|
||||
Kontrahent poNazwie = crm.Kontrahenci.WgNazwy["Firma XYZ"].FirstOrDefault();
|
||||
|
||||
// 3. Po NIP — filtr serwerowy; warunek aplikujemy na indeksie. Porównania tekstowe są case-insensitive
|
||||
var nip = Nip.Flat("123-456-32-18"); // usuwa myślniki
|
||||
Kontrahent poNip = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].FirstOrDefault();
|
||||
|
||||
// 4. Po fragmencie nazwy / mieście — serwerowy LIKE (warunek na indeksie WgNazwy)
|
||||
foreach (Kontrahent k in crm.Kontrahenci.WgNazwy[(Kontrahent k) =>
|
||||
k.Nazwa.Contains("bud") && k.Adres.Miejscowosc == "Kraków"])
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// 5. Dedup przed dodaniem nowego kontrahenta
|
||||
bool juzIstnieje = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].Any();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `WgKodu[...]` zwraca pojedynczy rekord (klucz unikalny) — może być `null`. `WgNazwy[...]` zwraca
|
||||
**zbiór** (klucz nieunikalny), trzeba `.FirstOrDefault()`/iterację.
|
||||
- **Nie iteruj całej tabeli** `Kontrahenci` z `if` w pamięci — to tabela kartotekowa (rośnie z
|
||||
biznesem). Filtruj przez warunek aplikowany **na indeksie**, np.
|
||||
`crm.Kontrahenci.WgKodu[(Kontrahent k) => …]` (warunek wykonywany przez SQL). Indeksator samej
|
||||
tabeli (`crm.Kontrahenci[…]`) służy do dostępu po `ID`/kluczu, nie przyjmuje wyrażenia LINQ.
|
||||
Patrz [`rowcondition.md`](../rowcondition.md) i [`safe-code.md`](../safe-code.md) §6.
|
||||
- W `RowCondition` (wyrażeniu LINQ) wolno użyć **tylko pól bazodanowych**. `NazwaFormatowana`,
|
||||
`KodKraju`, `Platnik` są kalkulowane → rzucą `LinqConditionException`.
|
||||
- Porównania tekstowe w warunku są **case-insensitive** — nie dubluj `ToLower()`.
|
||||
- Przed porównaniem NIP/EU VAT normalizuj wejście (`Nip.Flat`, `EuVat.Flat`), bo w bazie bywają
|
||||
formaty z myślnikami i bez.
|
||||
|
||||
### CRM-W2 — Walidacja NIP / REGON / EU VAT
|
||||
|
||||
**Cel:** sprawdzić poprawność NIP/REGON (suma kontrolna) i EU VAT (format/kraj) przed zapisem,
|
||||
niezależnie od weryfikacji online (CRM-W15).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Wejście | Metoda publiczna |
|
||||
|---|---|---|
|
||||
| NIP krajowy | 10 cyfr lub `DDD-DDD-DD-DD` | `Soneta.Core.Nip.Test(string)` |
|
||||
| REGON 9/14 | 9 lub 14 cyfr | `Soneta.Core.Regon.Test(string)` |
|
||||
| EU VAT | prefiks kraju + numer | `Soneta.Core.EuVat.Test(string, ISessionable)` |
|
||||
| Normalizacja | usunięcie myślników/spacji | `Nip.Flat`, `Nip.Format`, `EuVat.Flat` |
|
||||
| Rozbicie EU VAT | kraj + numer | `EuVat.Split(value, out country, out nip)` |
|
||||
|
||||
**Pola i typy:** `NIP: string`, `REGON: string`, `EuVAT: string`. Walidatory są **statyczne**;
|
||||
`EuVat.Test` wymaga `ISessionable` (sprawdza listę krajów UE w bazie).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
// Walidatory rzucają NullReferenceException dla null — najpierw odsiej puste wejście.
|
||||
if (!nip.IsNullOrEmpty() && Nip.Test(nip)) { /* NIP poprawny */ }
|
||||
if (!regon.IsNullOrEmpty() && Regon.Test(regon)) { /* REGON poprawny */ }
|
||||
if (!euVat.IsNullOrEmpty() && EuVat.Test(euVat, session)) { /* EU VAT poprawny */ }
|
||||
|
||||
// Rozbicie EU VAT "PL1234563218" -> kraj "PL", numer "1234563218"
|
||||
EuVat.Split(euVat, out string kodKraju, out string numer);
|
||||
|
||||
// Walidacja w event-handlerze zapisu (rzut PRZED Commit/Save):
|
||||
if (!kontrahent.NIP.IsNullOrEmpty() && !Nip.Test(kontrahent.NIP))
|
||||
throw new RowException(kontrahent, "Nieprawidłowy NIP".Translate(), nameof(kontrahent.NIP));
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `Nip.Test`, `Regon.Test`, `EuVat.Test` **rzucają `NullReferenceException` dla `null`** (odwołują się
|
||||
do `.Length`). Zawsze najpierw sprawdź `IsNullOrEmpty`.
|
||||
- To walidacja **formatu/sumy kontrolnej**, a nie weryfikacja w MF/VIES — patrz CRM-W15.
|
||||
- Komunikaty walidacyjne rzucaj jako `RowException(row, "…".Translate(), nameof(Pole))` **przed**
|
||||
`Commit()` (safe-code §5.1). Wyjątek po `Commit()` nie wycofa zmiany z sesji.
|
||||
- Ustawienie `NIP`/`EuVAT` na samym `Kontrahent` uruchamia wbudowaną synchronizację (NIP↔EuVAT,
|
||||
auto-zmiana `RodzajPodmiotu`) — własna walidacja jest dodatkiem, nie zastępstwem.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user