Skip to content

Commit d663408

Browse files
arg-resolve-auction-price-macro
1 parent ea32800 commit d663408

File tree

2 files changed

+262
-9
lines changed

2 files changed

+262
-9
lines changed

src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.prebid.server.util.BidderUtil;
3030
import org.prebid.server.util.HttpUtil;
3131

32+
import java.math.BigDecimal;
3233
import java.util.ArrayList;
3334
import java.util.Collection;
3435
import java.util.Collections;
@@ -44,6 +45,7 @@ public class TheTradeDeskBidder implements Bidder<BidRequest> {
4445

4546
private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
4647
private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$");
48+
private static final String PRICE_MACRO = "${AUCTION_PRICE}";
4749

4850
private final String endpointUrl;
4951
private final String supplyId;
@@ -180,32 +182,54 @@ private String resolveEndpoint(String sourceSupplyId) {
180182
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
181183
try {
182184
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
183-
return Result.withValues(extractBids(bidResponse));
184-
} catch (DecodeException | PreBidException e) {
185+
final List<BidderError> errors = new ArrayList<>();
186+
final List<BidderBid> bids = extractBids(bidResponse, errors);
187+
return Result.of(bids, errors);
188+
} catch (DecodeException e) {
185189
return Result.withError(BidderError.badServerResponse(e.getMessage()));
186190
}
187191
}
188192

189-
private static List<BidderBid> extractBids(BidResponse bidResponse) {
193+
private static List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
190194
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
191195
return Collections.emptyList();
192196
}
193197

194198
return bidResponse.getSeatbid().stream()
195199
.filter(Objects::nonNull)
196-
.map(SeatBid::getBid).filter(Objects::nonNull)
200+
.map(SeatBid::getBid)
201+
.filter(Objects::nonNull)
197202
.flatMap(Collection::stream)
198203
.filter(Objects::nonNull)
199-
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
204+
.map(bid -> {
205+
final BidType bidType = getBidType(bid, errors);
206+
return bidType != null ? BidderBid.of(resolvePriceMacros(bid), bidType, bidResponse.getCur())
207+
: null;
208+
})
209+
.filter(Objects::nonNull)
200210
.toList();
201211
}
202212

203-
private static BidType getBidType(Bid bid) {
213+
private static BidType getBidType(Bid bid, List<BidderError> errors) {
204214
return switch (bid.getMtype()) {
205215
case 1 -> BidType.banner;
206216
case 2 -> BidType.video;
207217
case 4 -> BidType.xNative;
208-
case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype()));
218+
case null, default -> {
219+
errors.add(BidderError.badServerResponse(
220+
"could not define media type for impression: " + bid.getImpid()));
221+
yield null;
222+
}
209223
};
210224
}
225+
226+
private static Bid resolvePriceMacros(Bid bid) {
227+
final BigDecimal price = bid.getPrice();
228+
final String priceAsString = price != null ? price.toPlainString() : "0";
229+
230+
return bid.toBuilder()
231+
.nurl(StringUtils.replace(bid.getNurl(), PRICE_MACRO, priceAsString))
232+
.adm(StringUtils.replace(bid.getAdm(), PRICE_MACRO, priceAsString))
233+
.build();
234+
}
211235
}

src/test/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidderTest.java

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
2525
import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk;
2626

27+
import java.math.BigDecimal;
2728
import java.util.Arrays;
2829
import java.util.List;
2930
import java.util.Set;
@@ -33,6 +34,7 @@
3334
import static java.util.function.UnaryOperator.identity;
3435
import static org.assertj.core.api.Assertions.assertThat;
3536
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
37+
import static org.assertj.core.api.Assertions.tuple;
3638
import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
3739
import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
3840
import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
@@ -458,7 +460,7 @@ public void makeBidsShouldReturnVideoBid() throws JsonProcessingException {
458460
}
459461

460462
@Test
461-
public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
463+
public void makeBidsShouldReturnErrorWhenMediaTypeIsMissing() throws JsonProcessingException {
462464
// given
463465
final BidderCall<BidRequest> httpCall = givenHttpCall(
464466
givenBidResponse(bidBuilder -> bidBuilder.impid("123")));
@@ -469,7 +471,36 @@ public void makeBidsShouldThrowErrorWhenMediaTypeIsMissing() throws JsonProcessi
469471
// then
470472
assertThat(result.getValue()).isEmpty();
471473
assertThat(result.getErrors()).hasSize(1)
472-
.containsOnly(BidderError.badServerResponse("unsupported mtype: null"));
474+
.containsOnly(BidderError.badServerResponse("could not define media type for impression: 123"));
475+
}
476+
477+
@Test
478+
public void makeBidsShouldReturnValidBidsAndErrorsForMixedMediaTypes() throws JsonProcessingException {
479+
// given
480+
final BidderCall<BidRequest> httpCall = givenHttpCall(
481+
mapper.writeValueAsString(BidResponse.builder()
482+
.cur("USD")
483+
.seatbid(singletonList(SeatBid.builder()
484+
.bid(Arrays.asList(
485+
Bid.builder().mtype(1).impid("valid1").build(), // valid banner
486+
Bid.builder().mtype(3).impid("invalid1").build(), // invalid mtype
487+
Bid.builder().mtype(2).impid("valid2").build(), // valid video
488+
Bid.builder().mtype(null).impid("invalid2").build() // null mtype
489+
))
490+
.build()))
491+
.build()));
492+
493+
// when
494+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
495+
496+
// then
497+
assertThat(result.getValue()).hasSize(2)
498+
.extracting(bidderBid -> bidderBid.getBid().getImpid())
499+
.containsExactly("valid1", "valid2");
500+
assertThat(result.getErrors()).hasSize(2)
501+
.containsExactly(
502+
BidderError.badServerResponse("could not define media type for impression: invalid1"),
503+
BidderError.badServerResponse("could not define media type for impression: invalid2"));
473504
}
474505

475506
private String givenBidResponse(UnaryOperator<Bid.BidBuilder> bidCustomizer) throws JsonProcessingException {
@@ -509,4 +540,202 @@ private static ObjectNode impExt(String publisherId, String supplySourceId) {
509540
return mapper.valueToTree(ExtPrebid.of(null, ExtImpTheTradeDesk.of(publisherId, supplySourceId)));
510541
}
511542

543+
@Test
544+
public void makeBidsShouldReplacePriceMacroInNurlAndAdmWithBidPrice() throws JsonProcessingException {
545+
// given
546+
final BidderCall<BidRequest> httpCall = givenHttpCall(
547+
givenBidResponse(bidBuilder -> bidBuilder
548+
.mtype(1)
549+
.impid("123")
550+
.price(BigDecimal.valueOf(1.23))
551+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
552+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
553+
554+
// when
555+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
556+
557+
// then
558+
assertThat(result.getErrors()).isEmpty();
559+
assertThat(result.getValue()).hasSize(1)
560+
.extracting(BidderBid::getBid)
561+
.extracting(Bid::getNurl, Bid::getAdm, Bid::getPrice)
562+
.containsOnly(tuple("http://example.com/nurl?price=1.23", "<div>Price: 1.23</div>", BigDecimal.valueOf(1.23)));
563+
}
564+
565+
@Test
566+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsNull() throws JsonProcessingException {
567+
// given
568+
final BidderCall<BidRequest> httpCall = givenHttpCall(
569+
givenBidResponse(bidBuilder -> bidBuilder
570+
.mtype(1)
571+
.impid("123")
572+
.price(null)
573+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
574+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
575+
576+
// when
577+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
578+
579+
// then
580+
assertThat(result.getErrors()).isEmpty();
581+
assertThat(result.getValue()).hasSize(1)
582+
.extracting(BidderBid::getBid)
583+
.extracting(Bid::getNurl, Bid::getAdm)
584+
.containsOnly(tuple("http://example.com/nurl?price=0", "<div>Price: 0</div>"));
585+
}
586+
587+
@Test
588+
public void makeBidsShouldReplacePriceMacroWithZeroWhenBidPriceIsZero() throws JsonProcessingException {
589+
// given
590+
final BidderCall<BidRequest> httpCall = givenHttpCall(
591+
givenBidResponse(bidBuilder -> bidBuilder
592+
.mtype(1)
593+
.impid("123")
594+
.price(BigDecimal.ZERO)
595+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
596+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
597+
598+
// when
599+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
600+
601+
// then
602+
assertThat(result.getErrors()).isEmpty();
603+
assertThat(result.getValue()).hasSize(1)
604+
.extracting(BidderBid::getBid)
605+
.extracting(Bid::getNurl, Bid::getAdm)
606+
.containsOnly(tuple("http://example.com/nurl?price=0", "<div>Price: 0</div>"));
607+
}
608+
609+
@Test
610+
public void makeBidsShouldReplacePriceMacroInNurlOnlyWhenAdmDoesNotContainMacro() throws JsonProcessingException {
611+
// given
612+
final BidderCall<BidRequest> httpCall = givenHttpCall(
613+
givenBidResponse(bidBuilder -> bidBuilder
614+
.mtype(1)
615+
.impid("123")
616+
.price(BigDecimal.valueOf(5.67))
617+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
618+
.adm("<div>No macro here</div>")));
619+
620+
// when
621+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
622+
623+
// then
624+
assertThat(result.getErrors()).isEmpty();
625+
assertThat(result.getValue()).hasSize(1)
626+
.extracting(BidderBid::getBid)
627+
.extracting(Bid::getNurl, Bid::getAdm)
628+
.containsOnly(tuple("http://example.com/nurl?price=5.67", "<div>No macro here</div>"));
629+
}
630+
631+
@Test
632+
public void makeBidsShouldReplacePriceMacroInAdmOnlyWhenNurlDoesNotContainMacro() throws JsonProcessingException {
633+
// given
634+
final BidderCall<BidRequest> httpCall = givenHttpCall(
635+
givenBidResponse(bidBuilder -> bidBuilder
636+
.mtype(1)
637+
.impid("123")
638+
.price(BigDecimal.valueOf(8.90))
639+
.nurl("http://example.com/nurl")
640+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
641+
642+
// when
643+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
644+
645+
// then
646+
assertThat(result.getErrors()).isEmpty();
647+
assertThat(result.getValue()).hasSize(1)
648+
.extracting(BidderBid::getBid)
649+
.extracting(Bid::getNurl, Bid::getAdm)
650+
.containsOnly(tuple("http://example.com/nurl", "<div>Price: 8.9</div>"));
651+
}
652+
653+
@Test
654+
public void makeBidsShouldNotReplacePriceMacroWhenNurlAndAdmDoNotContainMacro() throws JsonProcessingException {
655+
// given
656+
final BidderCall<BidRequest> httpCall = givenHttpCall(
657+
givenBidResponse(bidBuilder -> bidBuilder
658+
.mtype(1)
659+
.impid("123")
660+
.price(BigDecimal.valueOf(12.34))
661+
.nurl("http://example.com/nurl")
662+
.adm("<div>No macro</div>")));
663+
664+
// when
665+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
666+
667+
// then
668+
assertThat(result.getErrors()).isEmpty();
669+
assertThat(result.getValue()).hasSize(1)
670+
.extracting(BidderBid::getBid)
671+
.extracting(Bid::getNurl, Bid::getAdm)
672+
.containsOnly(tuple("http://example.com/nurl", "<div>No macro</div>"));
673+
}
674+
675+
@Test
676+
public void makeBidsShouldHandleNullNurlAndAdm() throws JsonProcessingException {
677+
// given
678+
final BidderCall<BidRequest> httpCall = givenHttpCall(
679+
givenBidResponse(bidBuilder -> bidBuilder
680+
.mtype(1)
681+
.impid("123")
682+
.price(BigDecimal.valueOf(15.00))
683+
.nurl(null)
684+
.adm(null)));
685+
686+
// when
687+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
688+
689+
// then
690+
assertThat(result.getErrors()).isEmpty();
691+
assertThat(result.getValue()).hasSize(1)
692+
.extracting(BidderBid::getBid)
693+
.extracting(Bid::getNurl, Bid::getAdm)
694+
.containsOnly(tuple(null, null));
695+
}
696+
697+
@Test
698+
public void makeBidsShouldReplaceMultiplePriceMacrosInSameField() throws JsonProcessingException {
699+
// given
700+
final BidderCall<BidRequest> httpCall = givenHttpCall(
701+
givenBidResponse(bidBuilder -> bidBuilder
702+
.mtype(1)
703+
.impid("123")
704+
.price(BigDecimal.valueOf(9.99))
705+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}&backup_price=${AUCTION_PRICE}")
706+
.adm("<div>Price: ${AUCTION_PRICE}, Fallback: ${AUCTION_PRICE}</div>")));
707+
708+
// when
709+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
710+
711+
// then
712+
assertThat(result.getErrors()).isEmpty();
713+
assertThat(result.getValue()).hasSize(1)
714+
.extracting(BidderBid::getBid)
715+
.extracting(Bid::getNurl, Bid::getAdm)
716+
.containsOnly(tuple("http://example.com/nurl?price=9.99&backup_price=9.99", "<div>Price: 9.99, Fallback: 9.99</div>"));
717+
}
718+
719+
@Test
720+
public void makeBidsShouldHandleLargeDecimalPrices() throws JsonProcessingException {
721+
// given
722+
final BidderCall<BidRequest> httpCall = givenHttpCall(
723+
givenBidResponse(bidBuilder -> bidBuilder
724+
.mtype(1)
725+
.impid("123")
726+
.price(new BigDecimal("123456789.123456789"))
727+
.nurl("http://example.com/nurl?price=${AUCTION_PRICE}")
728+
.adm("<div>Price: ${AUCTION_PRICE}</div>")));
729+
730+
// when
731+
final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
732+
733+
// then
734+
assertThat(result.getErrors()).isEmpty();
735+
assertThat(result.getValue()).hasSize(1)
736+
.extracting(BidderBid::getBid)
737+
.extracting(Bid::getNurl, Bid::getAdm)
738+
.containsOnly(tuple("http://example.com/nurl?price=123456789.123456789", "<div>Price: 123456789.123456789</div>"));
739+
}
740+
512741
}

0 commit comments

Comments
 (0)