package mynalog import ( "fmt" "net/url" "strings" "unicode/utf8" "github.com/google/uuid" ) // maxNameRunes is the tax service's ceiling on a service name, counted in characters rather than // bytes — the names here are Cyrillic, so the two differ by a factor of two. const maxNameRunes = 128 // markerRunes is how much of the order id rides in the name. // // It is taken from the END of the identifier, and that is not cosmetic. Our order ids are UUIDv7, // whose leading hex digits are a millisecond timestamp: the first eight of them are the high 32 // bits of that timestamp, so they only change about once a minute and every order placed within the // same minute would share them. The trailing digits are random. Since this marker is the sole key // the recovery probe matches on, a shared marker would make two purchases indistinguishable — which // is precisely the situation that ends in an income filed twice or not at all. const markerRunes = 8 // defaultTitle describes the goods when the product carries no usable title of its own. It has to // read as a plain description of what was sold, because the tax service and the buyer both see it. const defaultTitle = "Цифровые ценности" // IncomeName builds the service name for one purchase: a human-readable description of what was // sold, followed by a marker derived from the order id. // // The marker is not decoration. Registering an income is not idempotent, so after a timeout the // only way to discover whether the receipt exists is to search the tax service's income list by // this exact string — and without the marker two identical chip packs would be indistinguishable, // which would either duplicate the income or lose it. The result is capped at maxNameRunes with the // description giving way first: the marker is never truncated. // // chips is the pack size; when it is zero the product is not a chip pack and fallbackTitle // describes it instead. func IncomeName(chips int, orderID uuid.UUID, fallbackTitle string) string { desc := strings.TrimSpace(fallbackTitle) if chips > 0 { desc = fmt.Sprintf("Внутриигровая валюта: %q, %d шт.", "Фишка", chips) } if desc == "" { desc = defaultTitle } suffix := fmt.Sprintf(" (ID: %s)", orderMarker(orderID)) budget := maxNameRunes - utf8.RuneCountInString(suffix) if budget < 1 { return suffix } if utf8.RuneCountInString(desc) > budget { desc = strings.TrimSpace(string([]rune(desc)[:budget-1])) + "…" } return desc + suffix } // orderMarker renders the part of the order id that rides in a service name: its last markerRunes // hex digits, which for a UUIDv7 are random rather than clock-derived. func orderMarker(orderID uuid.UUID) string { s := orderID.String() if len(s) < markerRunes { return s } return s[len(s)-markerRunes:] } // ReceiptURL is where the printable receipt for a registered income lives. It is what the buyer is // sent as their proof of purchase, so it is a package function rather than a method: the letter is // composed far from any authenticated client. func ReceiptURL(baseURL, inn, receiptUUID string) string { if baseURL == "" { baseURL = DefaultBaseURL } return fmt.Sprintf("%s/api/v1/receipt/%s/%s/print", strings.TrimSuffix(baseURL, "/"), url.PathEscape(inn), url.PathEscape(receiptUUID)) } // ReceiptURL is where the printable receipt for a registered income lives, against this client's // service root. func (c *Client) ReceiptURL(inn, receiptUUID string) string { return ReceiptURL(c.baseURL, inn, receiptUUID) }