Входящие M2M: пакет только целиком и отказ M2M08 для бумаг «для квалифицированных инвесторов»
Сверка с DEPO: у бумаги проверяется опция RESTRICTED («Ограничение ценной бумаги по обращению», 1 — предназначена для квалифицированных инвесторов). Если инвестор не квалифицирован — отказ M2M08; квалифицирован по всем видам — проходит; по части видов — предупреждение, решает оператор. Пакет принимается только целиком (решение 16.09): при отказе хотя бы по одной бумаге рекомендация — отказ по всем с кодом первой отказной, и так же нормализуется отправляемое решение (packageDecisions); в форме выбор отказа по одной бумаге переключает на отказ все. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,7 @@ type depoParty struct {
|
||||
iisDet string
|
||||
qualOK bool
|
||||
qualWarn bool
|
||||
qualAll bool // квалифицирован по всем видам бумаг
|
||||
qualDet string
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ func depoResolveParty(ctx context.Context, db *sql.DB, fs FancyMSSQLSettings, se
|
||||
p.qualWarn, p.qualDet = true, "инвестор не найден — квал не проверялся"
|
||||
return p
|
||||
}
|
||||
p.qualWarn, p.qualDet = depoFaceQualified(ctx, db, p.faceID)
|
||||
p.qualWarn, p.qualAll, p.qualDet = depoFaceQualified(ctx, db, p.faceID)
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -169,8 +170,8 @@ var depoQualKinds = []string{
|
||||
|
||||
// depoFaceQualified — признак квалифицированного инвестора: действующая на сегодня опция лица
|
||||
// IS_QUAL («Вхождение в реестр лиц, признанных квалифицированными инвесторами»). Значение —
|
||||
// маска видов бумаг (511 и -1 — все виды, 0 — ни одного). Возвращает (предупреждение, пояснение).
|
||||
func depoFaceQualified(ctx context.Context, db *sql.DB, faceID int64) (bool, string) {
|
||||
// маска видов бумаг (511 и -1 — все виды, 0 — ни одного). Возвращает (предупреждение, по всем видам, пояснение).
|
||||
func depoFaceQualified(ctx context.Context, db *sql.DB, faceID int64) (warn, all bool, det string) {
|
||||
var val sql.NullString
|
||||
err := db.QueryRowContext(ctx, `SELECT TOP 1 CAST(o.VAL AS varchar(20)) FROM OD_OPTIONS o
|
||||
JOIN OP_FIELDS f ON f.ID = o.DESCR AND f.AS_NAME = 'IS_QUAL'
|
||||
@@ -178,21 +179,20 @@ func depoFaceQualified(ctx context.Context, db *sql.DB, faceID int64) (bool, str
|
||||
ORDER BY o.B_DATE DESC`, sql.Named("p1", faceID)).Scan(&val)
|
||||
const notQual = " — если бумага только для квал., нужен отказ M2M08; решает оператор"
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return true, "инвестор НЕ квалифицированный (нет опции «Вхождение в реестр квалифицированных инвесторов» в DEPO)" + notQual
|
||||
return true, false, "инвестор НЕ квалифицированный (нет опции «Вхождение в реестр квалифицированных инвесторов» в DEPO)" + notQual
|
||||
}
|
||||
if err != nil {
|
||||
return true, "признак квалифицированного инвестора не прочитан: " + quikErrorDetail(err)
|
||||
return true, false, "признак квалифицированного инвестора не прочитан: " + quikErrorDetail(err)
|
||||
}
|
||||
mask, e := strconv.ParseInt(strings.TrimSpace(val.String), 10, 64)
|
||||
if e != nil {
|
||||
return true, fmt.Sprintf("признак квалифицированного инвестора в DEPO не распознан (%q) — решает оператор", val.String)
|
||||
return true, false, fmt.Sprintf("признак квалифицированного инвестора в DEPO не распознан (%q) — решает оператор", val.String)
|
||||
}
|
||||
if mask == 0 {
|
||||
return true, "инвестор НЕ квалифицированный (опция в DEPO = 0)" + notQual
|
||||
return true, false, "инвестор НЕ квалифицированный (опция в DEPO = 0)" + notQual
|
||||
}
|
||||
all := mask < 0 || mask&511 == 511
|
||||
if all {
|
||||
return false, "инвестор — квалифицированный по всем видам бумаг (опция DEPO)"
|
||||
if mask < 0 || mask&511 == 511 {
|
||||
return false, true, "инвестор — квалифицированный по всем видам бумаг (опция DEPO)"
|
||||
}
|
||||
var kinds []string
|
||||
for bit, name := range depoQualKinds {
|
||||
@@ -200,7 +200,7 @@ func depoFaceQualified(ctx context.Context, db *sql.DB, faceID int64) (bool, str
|
||||
kinds = append(kinds, name)
|
||||
}
|
||||
}
|
||||
return false, "инвестор — квалифицированный (опция DEPO): " + strings.Join(kinds, "; ")
|
||||
return false, false, "инвестор — квалифицированный (опция DEPO): " + strings.Join(kinds, "; ")
|
||||
}
|
||||
|
||||
// depoAccountIIS — относится ли торговый счёт (OD_CLIENTS.ID) к договору ИИС: название договора
|
||||
@@ -248,6 +248,46 @@ func depoSecurity(ctx context.Context, db *sql.DB, isin string) (id int64, name
|
||||
return
|
||||
}
|
||||
|
||||
// depoSecurityQualOnly — бумага «Предназначена для квалифицированных инвесторов»: действующая на сегодня
|
||||
// опция бумаги RESTRICTED («Ограничение ценной бумаги по обращению», 1 — для квал. инвесторов).
|
||||
func depoSecurityQualOnly(ctx context.Context, db *sql.DB, valID int64) (bool, string) {
|
||||
var val sql.NullString
|
||||
err := db.QueryRowContext(ctx, `SELECT TOP 1 CAST(o.VAL AS varchar(20)) FROM OD_OPTIONS o
|
||||
JOIN OP_FIELDS f ON f.ID = o.DESCR AND f.AS_NAME = 'RESTRICTED'
|
||||
WHERE o.OBJECT = @p1 AND o.B_DATE <= GETDATE() AND (o.E_DATE IS NULL OR o.E_DATE > GETDATE())
|
||||
ORDER BY o.B_DATE DESC`, sql.Named("p1", valID)).Scan(&val)
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return false, "ограничений по обращению нет (опция DEPO)"
|
||||
case err != nil:
|
||||
return false, "ограничение бумаги по обращению не прочитано: " + quikErrorDetail(err)
|
||||
case strings.TrimSpace(val.String) == "1":
|
||||
return true, "бумага предназначена для квалифицированных инвесторов (опция DEPO)"
|
||||
}
|
||||
return false, "ограничений по обращению нет (опция DEPO)"
|
||||
}
|
||||
|
||||
// qualOnlyCheck — бумага только для квал. инвесторов против квалификации инвестора.
|
||||
func qualOnlyCheck(found, qualOnly bool, qdet string, p depoParty) depoCheck {
|
||||
c := depoCheck{Name: "Бумага только для квал. инвесторов", OK: true, Code: "M2M08", Detail: qdet}
|
||||
switch {
|
||||
case !found:
|
||||
c.Detail = "не проверяется — бумаги нет в справочнике"
|
||||
case !qualOnly:
|
||||
case !p.faceOK:
|
||||
c.Detail += " — инвестор не найден, квалификация не проверялась"
|
||||
case p.qualAll:
|
||||
c.Detail += " — инвестор квалифицированный по всем видам бумаг"
|
||||
case p.qualWarn:
|
||||
c.OK = false
|
||||
c.Detail += " — инвестор НЕ квалифицированный: операция нарушит закон РФ"
|
||||
default:
|
||||
c.Warn = true
|
||||
c.Detail += " — " + p.qualDet + "; вид бумаги сверяет оператор"
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// depoBlocking — есть ли блокировка бумаги на счёте (BLOCK_DATE либо остаток на 98070.05/.07/.08/.09).
|
||||
func depoBlocking(ctx context.Context, db *sql.DB, valID, clientID int64) (ok bool, det string) {
|
||||
var blockDate sql.NullTime
|
||||
@@ -281,22 +321,35 @@ func finishSec(sr *depoSecResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// finishResult — итог по переводу. Пакет принимается только целиком (решение 16.09.2026): если хотя
|
||||
// бы по одной бумаге отказ, отказ рекомендуется по всем бумагам с кодом первой отказной.
|
||||
func finishResult(res *depoResult) {
|
||||
confirmN, rejectN := 0, 0
|
||||
for _, s := range res.Secs {
|
||||
if s.Recommend == "confirm" {
|
||||
confirmN++
|
||||
} else {
|
||||
rejectN++
|
||||
first := -1
|
||||
for i, s := range res.Secs {
|
||||
if s.Recommend != "confirm" {
|
||||
first = i
|
||||
break
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case rejectN == 0 && confirmN > 0:
|
||||
case len(res.Secs) == 0:
|
||||
case first < 0:
|
||||
res.Recommend, res.Summary = "confirm", "принять"
|
||||
case confirmN == 0 && rejectN > 0:
|
||||
res.Recommend, res.Summary = "reject", "отказ "+res.Secs[0].RejectCode
|
||||
case confirmN > 0 && rejectN > 0:
|
||||
res.Recommend, res.Summary = "mixed", fmt.Sprintf("принять %d, отказ %d", confirmN, rejectN)
|
||||
default:
|
||||
code, sec := res.Secs[first].RejectCode, res.Secs[first].Security
|
||||
passed := 0
|
||||
for i := range res.Secs {
|
||||
if res.Secs[i].Recommend == "confirm" {
|
||||
passed++
|
||||
res.Secs[i].Recommend, res.Secs[i].RejectCode = "reject", code
|
||||
res.Secs[i].Checks = append(res.Secs[i].Checks, depoCheck{Name: "Пакет принимается только целиком", OK: false, Code: code,
|
||||
Detail: "сама бумага проверку прошла; отказ всем пакетом из-за " + sec + " (" + code + ")"})
|
||||
}
|
||||
}
|
||||
res.Recommend, res.Summary = "reject", "отказ "+code
|
||||
if passed > 0 {
|
||||
res.Summary = fmt.Sprintf("отказ пакетом %s (из-за %s)", code, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +405,11 @@ func runDepoCheck(it *IncomingTransfer) depoResult {
|
||||
Detail: pick3(found, vname+" (ID "+fmt.Sprint(valID)+")", "ISIN "+isin+" в справочнике OD_VALUES нет")})
|
||||
sr.Checks = append(sr.Checks, depoCheck{Name: "Принята на обслуживание (галка)", OK: accepted, Code: "M2M05",
|
||||
Detail: pick3(!found, "не проверяется — бумаги нет в справочнике", pick3(accepted, "галка стоит (IS_IN=1)", "бумага есть, но галка НЕ стоит (IS_IN≠1)"))})
|
||||
qualOnly, qdet := false, ""
|
||||
if found {
|
||||
qualOnly, qdet = depoSecurityQualOnly(ctx, db, valID)
|
||||
}
|
||||
sr.Checks = append(sr.Checks, qualOnlyCheck(found, qualOnly, qdet, p))
|
||||
sr.Checks = append(sr.Checks, partyChecks(p, it.DocSeries, it.DocNumber, fio)...)
|
||||
blockOK, blockDet := true, "не проверяется — нет открытого счёта или бумаги"
|
||||
if p.accOpen != "" && found {
|
||||
|
||||
Reference in New Issue
Block a user