Skip to content

Commit c915cb2

Browse files
NGR-3177 Pro-tem reference scheme
1 parent f45d5ed commit c915cb2

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package uk.gov.hmrc.ngrraldfrontend.utils
18+
19+
import java.security.SecureRandom
20+
21+
object UniqueIdGenerator {
22+
23+
val allowedChars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
24+
private val generator = new SecureRandom()
25+
private val referenceLength = 12
26+
private val groupSize = 4
27+
28+
def generateId: String = {
29+
val raw = (1 to referenceLength)
30+
.map(_ => allowedChars(generator.nextInt(allowedChars.length)))
31+
.mkString
32+
33+
format(raw)
34+
}
35+
36+
def validateId(id: String): Either[Error, String] = {
37+
val sanitised = id.replaceAll("\\s", "").replaceAll("-", "").toUpperCase
38+
39+
if (sanitised.length != referenceLength || !sanitised.forall(allowedChars.contains(_)))
40+
Left(new Error("Invalid reference"))
41+
else
42+
Right(format(sanitised))
43+
}
44+
45+
def format(raw: String): String =
46+
raw.grouped(groupSize).mkString("-")
47+
48+
def parse(formatted: String): String =
49+
formatted.replaceAll("-", "").toUpperCase
50+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package uk.gov.hmrc.ngrraldfrontend.util
18+
19+
import org.scalatest.freespec.AnyFreeSpec
20+
import org.scalatest.matchers.must.Matchers
21+
import uk.gov.hmrc.ngrraldfrontend.utils.UniqueIdGenerator
22+
23+
class UniqueIdGeneratorSpec extends AnyFreeSpec with Matchers {
24+
25+
private val allowedChars = UniqueIdGenerator.allowedChars
26+
27+
"UniqueIdGenerator" - {
28+
29+
"generate a 12-char ID with 2 hyphens in correct format" in {
30+
val id = UniqueIdGenerator.generateId
31+
id.length mustBe 14
32+
id.count(_ == '-') mustBe 2
33+
34+
val compactId = UniqueIdGenerator.parse(id)
35+
compactId.length mustBe 12
36+
compactId.forall(allowedChars.contains(_)) mustBe true
37+
val formatted = UniqueIdGenerator.format(compactId)
38+
formatted mustBe id
39+
}
40+
41+
"validate good IDs" in {
42+
val validIds = List(
43+
"fdfd-fdfd-dfdf",
44+
"VDJ4-5NSG-8RHW",
45+
"BDJ6867MLMNE",
46+
"nvjf5245bsmv"
47+
)
48+
49+
validIds.foreach { id =>
50+
withClue(s"Expected '$id' to be valid: ") {
51+
UniqueIdGenerator.validateId(id).isRight mustBe true
52+
}
53+
}
54+
}
55+
56+
"invalidate bad IDs" in {
57+
val invalidIds = List(
58+
"0FDE-DFD1-DGJ1",
59+
"0efkdkfvncma",
60+
"hello",
61+
"&fdh-9adf-4jnf",
62+
"ABCD-EFGH-IJKLM",
63+
"ABCD-EFGH-IJ1M",
64+
"ABCD-EFGH-IJOM"
65+
)
66+
67+
invalidIds.foreach { id =>
68+
withClue(s"Expected '$id' to be invalid: ") {
69+
UniqueIdGenerator.validateId(id).isLeft mustBe true
70+
}
71+
}
72+
}
73+
74+
"format raw reference correctly" in {
75+
val raw = "7GQX2MZKJH9B"
76+
val formatted = UniqueIdGenerator.format(raw)
77+
formatted mustBe "7GQX-2MZK-JH9B"
78+
}
79+
80+
"parse formatted reference back to raw" in {
81+
val formatted = "7GQX-2MZK-JH9B"
82+
val raw = UniqueIdGenerator.parse(formatted)
83+
raw mustBe "7GQX2MZKJH9B"
84+
}
85+
86+
"round-trip format and parse should preserve original raw reference" in {
87+
val raw = "N8V3W5Y2X4ZT"
88+
val formatted = UniqueIdGenerator.format(raw)
89+
val parsed = UniqueIdGenerator.parse(formatted)
90+
parsed mustBe raw
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)