|
1 | 1 | package org.prebid.server.bidder.resetdigital; |
2 | 2 |
|
3 | | -import com.fasterxml.jackson.core.type.TypeReference; |
4 | 3 | import com.iab.openrtb.request.BidRequest; |
5 | 4 | import com.iab.openrtb.request.Imp; |
6 | 5 | import com.iab.openrtb.response.Bid; |
7 | 6 | import com.iab.openrtb.response.BidResponse; |
8 | 7 | import com.iab.openrtb.response.SeatBid; |
9 | | -import io.vertx.core.MultiMap; |
10 | 8 | import org.apache.commons.collections4.CollectionUtils; |
11 | 9 | import org.apache.commons.lang3.StringUtils; |
12 | 10 | import org.prebid.server.bidder.Bidder; |
13 | 11 | import org.prebid.server.bidder.model.BidderBid; |
14 | 12 | import org.prebid.server.bidder.model.BidderCall; |
15 | 13 | import org.prebid.server.bidder.model.BidderError; |
16 | 14 | import org.prebid.server.bidder.model.HttpRequest; |
| 15 | +import org.prebid.server.bidder.model.Price; |
17 | 16 | import org.prebid.server.bidder.model.Result; |
| 17 | +import org.prebid.server.currency.CurrencyConversionService; |
18 | 18 | import org.prebid.server.exception.PreBidException; |
19 | 19 | import org.prebid.server.json.DecodeException; |
20 | 20 | import org.prebid.server.json.JacksonMapper; |
21 | | -import org.prebid.server.proto.openrtb.ext.ExtPrebid; |
22 | | -import org.prebid.server.proto.openrtb.ext.request.resetdigital.ExtImpResetDigital; |
23 | 21 | import org.prebid.server.proto.openrtb.ext.response.BidType; |
24 | 22 | import org.prebid.server.util.BidderUtil; |
25 | 23 | import org.prebid.server.util.HttpUtil; |
26 | 24 |
|
| 25 | +import java.math.BigDecimal; |
27 | 26 | import java.util.ArrayList; |
| 27 | +import java.util.Collection; |
28 | 28 | import java.util.Collections; |
29 | 29 | import java.util.List; |
30 | 30 | import java.util.Objects; |
31 | | -import java.util.Optional; |
| 31 | +import java.util.stream.Stream; |
32 | 32 |
|
33 | 33 | public class ResetDigitalBidder implements Bidder<BidRequest> { |
34 | 34 |
|
35 | 35 | private static final String DEFAULT_CURRENCY = "USD"; |
36 | | - private static final TypeReference<ExtPrebid<?, ExtImpResetDigital>> EXT_TYPE_REFERENCE = |
37 | | - new TypeReference<>() { |
38 | | - }; |
39 | 36 |
|
40 | 37 | private final String endpointUrl; |
| 38 | + private final CurrencyConversionService currencyConversionService; |
41 | 39 | private final JacksonMapper mapper; |
42 | 40 |
|
43 | | - public ResetDigitalBidder(String endpointUrl, JacksonMapper mapper) { |
| 41 | + public ResetDigitalBidder(String endpointUrl, |
| 42 | + CurrencyConversionService currencyConversionService, |
| 43 | + JacksonMapper mapper) { |
| 44 | + |
44 | 45 | this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); |
| 46 | + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); |
45 | 47 | this.mapper = Objects.requireNonNull(mapper); |
46 | 48 | } |
47 | 49 |
|
48 | 50 | @Override |
49 | 51 | public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) { |
50 | | - if (request.getImp().size() != 1) { |
51 | | - return Result.withError(BidderError.badInput( |
52 | | - "ResetDigital adapter supports only one impression per request")); |
| 52 | + final List<Imp> bannerImps = new ArrayList<>(); |
| 53 | + final List<Imp> videoImps = new ArrayList<>(); |
| 54 | + final List<Imp> audioImps = new ArrayList<>(); |
| 55 | + Price bidFloorPrice; |
| 56 | + |
| 57 | + for (Imp imp : request.getImp()) { |
| 58 | + try { |
| 59 | + bidFloorPrice = resolveBidFloor(imp, request); |
| 60 | + } catch (PreBidException e) { |
| 61 | + return Result.withError(BidderError.badInput(e.getMessage())); |
| 62 | + } |
| 63 | + populateBannerImps(bannerImps, bidFloorPrice, imp); |
| 64 | + populateVideoImps(videoImps, bidFloorPrice, imp); |
| 65 | + populateAudiImps(audioImps, bidFloorPrice, imp); |
53 | 66 | } |
54 | 67 |
|
55 | | - final Imp imp = request.getImp().getFirst(); |
56 | | - final ExtImpResetDigital extImp; |
57 | | - try { |
58 | | - extImp = parseImpExt(imp); |
59 | | - } catch (PreBidException e) { |
60 | | - return Result.withError(BidderError.badInput(e.getMessage())); |
61 | | - } |
| 68 | + return Result.withValues(getHttpRequests(request, bannerImps, videoImps, audioImps)); |
| 69 | + } |
62 | 70 |
|
63 | | - final Imp modifiedImp = modifyImp(imp, extImp); |
64 | | - final BidRequest outgoingRequest = request.toBuilder() |
65 | | - .imp(Collections.singletonList(modifiedImp)) |
66 | | - .build(); |
| 71 | + private List<HttpRequest<BidRequest>> getHttpRequests(BidRequest request, |
| 72 | + List<Imp> bannerImps, |
| 73 | + List<Imp> videoImps, |
| 74 | + List<Imp> audioImps) { |
| 75 | + |
| 76 | + return Stream.of(bannerImps, videoImps, audioImps) |
| 77 | + .filter(CollectionUtils::isNotEmpty) |
| 78 | + .map(imp -> makeHttpRequest(request, imp)) |
| 79 | + .toList(); |
| 80 | + } |
67 | 81 |
|
68 | | - final String uri = endpointUrl + "?pid=" + HttpUtil.encodeUrl(extImp.getPlacementId()); |
69 | | - final MultiMap headers = HttpUtil.headers() |
70 | | - .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); |
| 82 | + private HttpRequest<BidRequest> makeHttpRequest(BidRequest bidRequest, List<Imp> imp) { |
| 83 | + final BidRequest outgoingRequest = bidRequest.toBuilder().imp(imp).build(); |
71 | 84 |
|
72 | | - return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, headers, uri, mapper)); |
| 85 | + return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); |
73 | 86 | } |
74 | 87 |
|
75 | | - private ExtImpResetDigital parseImpExt(Imp imp) { |
| 88 | + private static Imp modifyImp(Imp imp, Price bidFloorPrice) { |
| 89 | + return imp.toBuilder() |
| 90 | + .bidfloorcur(bidFloorPrice.getCurrency()) |
| 91 | + .bidfloor(bidFloorPrice.getValue()) |
| 92 | + .build(); |
| 93 | + } |
| 94 | + |
| 95 | + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { |
| 96 | + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); |
| 97 | + return BidderUtil.isValidPrice(initialBidFloorPrice) |
| 98 | + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest) |
| 99 | + : initialBidFloorPrice; |
| 100 | + } |
| 101 | + |
| 102 | + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { |
| 103 | + final String bidFloorCur = bidFloorPrice.getCurrency(); |
76 | 104 | try { |
77 | | - return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); |
78 | | - } catch (IllegalArgumentException e) { |
79 | | - throw new PreBidException("Error parsing resetDigitalExt from imp.ext: " + e.getMessage()); |
| 105 | + final BigDecimal convertedPrice = currencyConversionService |
| 106 | + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, DEFAULT_CURRENCY); |
| 107 | + |
| 108 | + return Price.of(DEFAULT_CURRENCY, convertedPrice); |
| 109 | + } catch (PreBidException e) { |
| 110 | + throw new PreBidException( |
| 111 | + "Unable to convert provided bid floor currency from %s to %s for imp `%s`" |
| 112 | + .formatted(bidFloorCur, DEFAULT_CURRENCY, impId)); |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + private static void populateBannerImps(List<Imp> bannerImps, Price bidFloorPrice, Imp imp) { |
| 117 | + if (imp.getBanner() != null) { |
| 118 | + final Imp bannerImp = imp.toBuilder().video(null).xNative(null).audio(null).build(); |
| 119 | + bannerImps.add(modifyImp(bannerImp, bidFloorPrice)); |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + private static void populateVideoImps(List<Imp> videoImps, Price bidFloorPrice, Imp imp) { |
| 124 | + if (imp.getVideo() != null) { |
| 125 | + final Imp videoImp = imp.toBuilder().banner(null).xNative(null).audio(null).build(); |
| 126 | + videoImps.add(modifyImp(videoImp, bidFloorPrice)); |
80 | 127 | } |
81 | 128 | } |
82 | 129 |
|
83 | | - private static Imp modifyImp(Imp imp, ExtImpResetDigital extImp) { |
84 | | - return StringUtils.isBlank(imp.getTagid()) |
85 | | - ? imp.toBuilder().tagid(extImp.getPlacementId()).build() |
86 | | - : imp; |
| 130 | + private static void populateAudiImps(List<Imp> audioImps, Price bidFloorPrice, Imp imp) { |
| 131 | + if (imp.getAudio() != null) { |
| 132 | + final Imp audioImp = imp.toBuilder().banner(null).xNative(null).video(null).build(); |
| 133 | + audioImps.add(modifyImp(audioImp, bidFloorPrice)); |
| 134 | + } |
87 | 135 | } |
88 | 136 |
|
89 | 137 | @Override |
90 | 138 | public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) { |
91 | 139 | try { |
92 | | - final List<BidderError> errors = new ArrayList<>(); |
93 | 140 | final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); |
94 | | - return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload(), errors), errors); |
| 141 | + return Result.withValues(extractBids(bidResponse, httpCall.getRequest().getPayload())); |
95 | 142 | } catch (DecodeException | PreBidException e) { |
96 | 143 | return Result.withError(BidderError.badServerResponse(e.getMessage())); |
97 | 144 | } |
98 | 145 | } |
99 | 146 |
|
100 | | - private static List<BidderBid> extractBids(BidResponse bidResponse, |
101 | | - BidRequest bidRequest, |
102 | | - List<BidderError> errors) { |
103 | | - |
| 147 | + private static List<BidderBid> extractBids(BidResponse bidResponse, BidRequest bidRequest) { |
104 | 148 | if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { |
105 | 149 | return Collections.emptyList(); |
106 | 150 | } |
107 | | - |
108 | | - final Imp imp = bidRequest.getImp().getFirst(); |
109 | | - final String currency = StringUtils.isNotBlank(bidResponse.getCur()) |
110 | | - ? bidResponse.getCur() |
111 | | - : bidRequest.getCur().stream().findFirst().orElse(DEFAULT_CURRENCY); |
112 | | - |
113 | | - final List<BidderBid> bidderBids = new ArrayList<>(); |
114 | | - for (SeatBid seatBid : bidResponse.getSeatbid()) { |
115 | | - if (seatBid == null || CollectionUtils.isEmpty(seatBid.getBid())) { |
116 | | - continue; |
117 | | - } |
118 | | - |
119 | | - for (Bid bid : seatBid.getBid()) { |
120 | | - try { |
121 | | - bidderBids.add(makeBidderBid(bid, seatBid.getSeat(), currency, imp)); |
122 | | - } catch (PreBidException e) { |
123 | | - errors.add(BidderError.badServerResponse(e.getMessage())); |
124 | | - } |
125 | | - } |
| 151 | + if (bidResponse.getCur() != null && !StringUtils.equalsIgnoreCase(DEFAULT_CURRENCY, bidResponse.getCur())) { |
| 152 | + throw new PreBidException("Bidder support only USD currency"); |
126 | 153 | } |
127 | | - |
128 | | - return bidderBids; |
| 154 | + return bidsFromResponse(bidResponse, bidRequest); |
129 | 155 | } |
130 | 156 |
|
131 | | - private static BidderBid makeBidderBid(Bid bid, String seat, String currency, Imp imp) { |
132 | | - if (!BidderUtil.isValidPrice(bid.getPrice())) { |
133 | | - throw new PreBidException("price %s <= 0 filtered out".formatted(bid.getPrice())); |
134 | | - } |
135 | | - |
136 | | - final BidType bidType = Optional.ofNullable(getBidType(bid)) |
137 | | - .orElseGet(() -> getBidType(bid, imp)); |
138 | | - |
139 | | - return StringUtils.isNotBlank(seat) |
140 | | - ? BidderBid.of(bid, bidType, seat, currency) |
141 | | - : BidderBid.of(bid, bidType, currency); |
142 | | - } |
143 | | - |
144 | | - private static BidType getBidType(Bid bid) { |
145 | | - final Integer mtype = bid.getMtype(); |
146 | | - return switch (mtype) { |
147 | | - case 1 -> BidType.banner; |
148 | | - case 2 -> BidType.video; |
149 | | - case 3 -> BidType.audio; |
150 | | - case 4 -> BidType.xNative; |
151 | | - case null -> null; |
152 | | - default -> throw new PreBidException("Unsupported MType: " + mtype); |
153 | | - }; |
| 157 | + private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) { |
| 158 | + return bidResponse.getSeatbid().stream() |
| 159 | + .filter(Objects::nonNull) |
| 160 | + .map(SeatBid::getBid) |
| 161 | + .filter(Objects::nonNull) |
| 162 | + .flatMap(Collection::stream) |
| 163 | + .map(bid -> BidderBid.of(bid, getBidType(bid, bidRequest.getImp()), DEFAULT_CURRENCY)) |
| 164 | + .toList(); |
154 | 165 | } |
155 | 166 |
|
156 | | - private static BidType getBidType(Bid bid, Imp imp) { |
157 | | - if (!imp.getId().equals(bid.getImpid())) { |
158 | | - throw new PreBidException("No matching impression found for ImpID: " + bid.getImpid()); |
159 | | - } |
160 | | - |
161 | | - if (imp.getVideo() != null) { |
162 | | - return BidType.video; |
163 | | - } else if (imp.getAudio() != null) { |
164 | | - return BidType.audio; |
165 | | - } else if (imp.getXNative() != null) { |
166 | | - return BidType.xNative; |
| 167 | + private static BidType getBidType(Bid bid, List<Imp> imps) { |
| 168 | + final String impId = bid.getImpid(); |
| 169 | + for (Imp imp : imps) { |
| 170 | + if (imp.getId().equals(impId)) { |
| 171 | + if (imp.getBanner() != null) { |
| 172 | + return BidType.banner; |
| 173 | + } else if (imp.getVideo() != null) { |
| 174 | + return BidType.video; |
| 175 | + } else if (imp.getAudio() != null) { |
| 176 | + return BidType.audio; |
| 177 | + } |
| 178 | + } |
167 | 179 | } |
168 | | - return BidType.banner; |
| 180 | + throw new PreBidException("Failed to find banner/video/audio impression " + impId); |
169 | 181 | } |
170 | 182 | } |
0 commit comments